Quellcode durchsuchen

ScrollingContainer & SubsetsMenu && 20w18b

Signed-off-by: shedaniel <daniel@shedaniel.me>
shedaniel vor 5 Jahren
Ursprung
Commit
21cce68f1b
28 geänderte Dateien mit 1339 neuen und 801 gelöschten Zeilen
  1. 3 5
      README.md
  2. 7 7
      gradle.properties
  3. 17 10
      src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCore.java
  4. 3 0
      src/main/java/me/shedaniel/rei/api/ConfigObject.java
  5. 40 0
      src/main/java/me/shedaniel/rei/api/subsets/SubsetsRegistry.java
  6. 74 24
      src/main/java/me/shedaniel/rei/gui/ContainerScreenOverlay.java
  7. 30 102
      src/main/java/me/shedaniel/rei/gui/VillagerRecipeViewingScreen.java
  8. 39 136
      src/main/java/me/shedaniel/rei/gui/config/entry/FilteringEntry.java
  9. 256 0
      src/main/java/me/shedaniel/rei/gui/subsets/SubsetsMenu.java
  10. 21 0
      src/main/java/me/shedaniel/rei/gui/subsets/SubsetsMenuEntry.java
  11. 117 0
      src/main/java/me/shedaniel/rei/gui/subsets/entries/EntryStackMenuEntry.java
  12. 209 0
      src/main/java/me/shedaniel/rei/gui/subsets/entries/SubMenuEntry.java
  13. 34 130
      src/main/java/me/shedaniel/rei/gui/widget/EntryListWidget.java
  14. 7 3
      src/main/java/me/shedaniel/rei/gui/widget/EntryWidget.java
  15. 32 127
      src/main/java/me/shedaniel/rei/gui/widget/FavoritesListWidget.java
  16. 0 1
      src/main/java/me/shedaniel/rei/gui/widget/LateRenderable.java
  17. 180 0
      src/main/java/me/shedaniel/rei/gui/widget/ScrollingContainer.java
  18. 1 3
      src/main/java/me/shedaniel/rei/impl/ClientHelperImpl.java
  19. 25 10
      src/main/java/me/shedaniel/rei/impl/ConfigObjectImpl.java
  20. 2 1
      src/main/java/me/shedaniel/rei/impl/EntryRegistryImpl.java
  21. 71 9
      src/main/java/me/shedaniel/rei/impl/InternalWidgets.java
  22. 3 0
      src/main/java/me/shedaniel/rei/impl/RecipeHelperImpl.java
  23. 7 0
      src/main/java/me/shedaniel/rei/impl/SearchArgument.java
  24. 83 0
      src/main/java/me/shedaniel/rei/impl/subsets/SubsetsRegistryImpl.java
  25. 5 17
      src/main/java/me/shedaniel/rei/plugin/DefaultPlugin.java
  26. 32 108
      src/main/java/me/shedaniel/rei/plugin/beacon/DefaultBeaconBaseCategory.java
  27. 35 108
      src/main/java/me/shedaniel/rei/plugin/information/DefaultInformationCategory.java
  28. 6 0
      src/main/resources/assets/roughlyenoughitems/lang/en_us.json

+ 3 - 5
README.md

@@ -1,17 +1,15 @@
 # Roughly Enough Items
-![Lines of Code](https://tokei.rs/b1/github/shedaniel/RoughlyEnoughItems?category=code)
-
 https://minecraft.curseforge.com/projects/roughly-enough-items <br>
-Roughly Enough Items is a mod to view Items and Recipes for Minecraft 1.13 - 1.15, supporting mod loaders from Forge, Rift to Fabric.
+Roughly Enough Items is a mod to view Items and Recipes for Minecraft 1.13 - 1.16, supporting mod loaders from Forge, Rift to Fabric.
 
 [Help translate REI on Crowdin!](https://crowdin.com/project/roughly-enough-items)
 
 ### Dependencies
 1.13 version requires [Rift](https://minecraft.curseforge.com/projects/rift).  
 1.13.2 version requires [Chocohead's Rift Fork](https://github.com/Chocohead/Rift) or [Forge 1.13.2](https://files.minecraftforge.net/maven/net/minecraftforge/forge/index_1.13.2.html).  
-1.14 and 1.15 version requires [Fabric Mod Loader](https://fabricmc.net/) and [Fabric API](https://minecraft.curseforge.com/projects/fabric).
+1.14, 1.15 and 1.16 version requires [Fabric Mod Loader](https://fabricmc.net/) and [Fabric API](https://minecraft.curseforge.com/projects/fabric).
 
-![](https://cdn.discordapp.com/attachments/472670263234920449/536198766118830081/unknown.png)
+![](https://i.imgur.com/OcOQLip.png)
 
 This mod is both client sided and server sided.
 

+ 7 - 7
gradle.properties

@@ -1,11 +1,11 @@
-mod_version=4.1.1-unstable
-minecraft_version=20w12a
-yarn_version=20w12a+build.3
-fabricloader_version=0.7.8+build.187
-cloth_events_version=2.0.0-unstable.202003051905
+mod_version=4.1.2-unstable
+minecraft_version=20w13b
+yarn_version=20w13b+build.4
+fabricloader_version=0.7.8+build.189
+cloth_events_version=2.0.2-unstable
 cloth_config_version=3.1.0-unstable
-modmenu_version=1.10.2+build.1
-fabric_api=0.5.4+build.310-1.16
+modmenu_version=1.11.0+build.2
+fabric_api=0.5.6+build.313-1.16
 autoconfig1u=1.2.4
 api_include=me.shedaniel.cloth:cloth-events,me.shedaniel.cloth:config-2,me.sargunvohra.mcmods:autoconfig1u,org.jetbrains:annotations
 api_exculde=

+ 17 - 10
src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCore.java

@@ -63,10 +63,7 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.jetbrains.annotations.ApiStatus;
 
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -81,6 +78,8 @@ public class RoughlyEnoughItemsCore implements ClientModInitializer {
     private static final Map<Identifier, REIPluginEntry> plugins = Maps.newHashMap();
     private static final ExecutorService SYNC_RECIPES = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "REI-SyncRecipes"));
     private static ConfigManager configManager;
+    @ApiStatus.Experimental
+    public static boolean isLeftModePressed = false;
     
     @ApiStatus.Internal
     public static RecipeHelper getRecipeHelper() {
@@ -248,12 +247,15 @@ public class RoughlyEnoughItemsCore implements ClientModInitializer {
     }
     
     private boolean shouldReturn(Class<?> screen) {
-        for (OverlayDecider decider : DisplayHelper.getInstance().getAllOverlayDeciders()) {
-            if (!decider.isHandingScreen(screen))
-                continue;
-            ActionResult result = decider.shouldScreenBeOverlayed(screen);
-            if (result != ActionResult.PASS)
-                return result == ActionResult.FAIL || ScreenHelper.getLastHandledScreen() == null;
+        try {
+            for (OverlayDecider decider : DisplayHelper.getInstance().getAllOverlayDeciders()) {
+                if (!decider.isHandingScreen(screen))
+                    continue;
+                ActionResult result = decider.shouldScreenBeOverlayed(screen);
+                if (result != ActionResult.PASS)
+                    return result == ActionResult.FAIL || ScreenHelper.getLastHandledScreen() == null;
+            }
+        } catch (ConcurrentModificationException ignored) {
         }
         return true;
     }
@@ -298,6 +300,7 @@ public class RoughlyEnoughItemsCore implements ClientModInitializer {
             return ActionResult.PASS;
         });
         ClothClientHooks.SCREEN_MOUSE_CLICKED.register((minecraftClient, screen, v, v1, i) -> {
+            isLeftModePressed = true;
             if (screen instanceof CreativeInventoryScreen)
                 if (ScreenHelper.isOverlayVisible() && ScreenHelper.getLastOverlay().mouseClicked(v, v1, i)) {
                     screen.setFocused(ScreenHelper.getLastOverlay());
@@ -307,6 +310,10 @@ public class RoughlyEnoughItemsCore implements ClientModInitializer {
                 }
             return ActionResult.PASS;
         });
+        ClothClientHooks.SCREEN_MOUSE_RELEASED.register((minecraftClient, screen, v, v1, i) -> {
+            isLeftModePressed = false;
+            return ActionResult.PASS;
+        });
         ClothClientHooks.SCREEN_MOUSE_SCROLLED.register((minecraftClient, screen, v, v1, v2) -> {
             if (shouldReturn(screen.getClass()))
                 return ActionResult.PASS;

+ 3 - 0
src/main/java/me/shedaniel/rei/api/ConfigObject.java

@@ -152,4 +152,7 @@ public interface ConfigObject {
     
     @ApiStatus.Experimental
     boolean doDebugSearchTimeRequired();
+    
+    @ApiStatus.Experimental
+    boolean isSubsetsEnabled();
 }

+ 40 - 0
src/main/java/me/shedaniel/rei/api/subsets/SubsetsRegistry.java

@@ -0,0 +1,40 @@
+package me.shedaniel.rei.api.subsets;
+
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.impl.subsets.SubsetsRegistryImpl;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+@ApiStatus.Experimental
+public interface SubsetsRegistry {
+    SubsetsRegistry INSTANCE = new SubsetsRegistryImpl();
+    
+    /**
+     * Gets all paths an entry is in, note that this is a really slow call as it looks through all paths.
+     */
+    @NotNull
+    List<String> getEntryPaths(@NotNull EntryStack stack);
+    
+    @Nullable
+    Set<EntryStack> getPathEntries(@NotNull String path);
+    
+    @NotNull
+    Set<EntryStack> getOrCreatePathEntries(@NotNull String path);
+    
+    @NotNull
+    Set<String> getPaths();
+    
+    void registerPathEntry(@NotNull String path, @NotNull EntryStack stack);
+    
+    void registerPathEntries(@NotNull String path, @NotNull Collection<EntryStack> stacks);
+    
+    default void registerPathEntries(@NotNull String path, @NotNull EntryStack... stacks) {
+        registerPathEntries(path, Arrays.asList(stacks));
+    }
+}

+ 74 - 24
src/main/java/me/shedaniel/rei/gui/ContainerScreenOverlay.java

@@ -34,7 +34,9 @@ import me.shedaniel.rei.api.widgets.Button;
 import me.shedaniel.rei.api.widgets.Tooltip;
 import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.config.SearchFieldLocation;
+import me.shedaniel.rei.gui.subsets.SubsetsMenu;
 import me.shedaniel.rei.gui.widget.*;
+import me.shedaniel.rei.impl.ClientHelperImpl;
 import me.shedaniel.rei.impl.InternalWidgets;
 import me.shedaniel.rei.impl.ScreenHelper;
 import me.shedaniel.rei.impl.Weather;
@@ -110,8 +112,13 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
     };
     private Rectangle bounds;
     private Window window;
-    private List<LateRenderable> lateRenderables = Lists.newArrayList();
     private Button leftButton, rightButton;
+    @ApiStatus.Experimental
+    private Rectangle subsetsButtonBounds;
+    @ApiStatus.Experimental
+    @Nullable
+    private SubsetsMenu subsetsMenu = null;
+    private Widget wrappedSubsetsMenu = null;
     
     public static EntryListWidget getEntryListWidget() {
         return ENTRY_LIST_WIDGET;
@@ -122,6 +129,12 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
         return favoritesListWidget;
     }
     
+    @ApiStatus.Experimental
+    @Nullable
+    public SubsetsMenu getSubsetsMenu() {
+        return subsetsMenu;
+    }
+    
     public void init(boolean useless) {
         init();
     }
@@ -130,6 +143,8 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
         this.shouldReInit = false;
         //Update Variables
         this.children().clear();
+        this.wrappedSubsetsMenu = null;
+        this.subsetsMenu = null;
         this.window = MinecraftClient.getInstance().getWindow();
         @SuppressWarnings({"RawTypeCanBeGeneric", "rawtypes"})
         DisplayHelper.DisplayBoundsHandler boundsHandler = DisplayHelper.getInstance().getResponsibleBoundsHandler(MinecraftClient.getInstance().currentScreen.getClass());
@@ -171,8 +186,8 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
         }
         
         final Rectangle configButtonArea = getConfigButtonArea();
-        LateRenderable tmp;
-        widgets.add((Widget) (tmp = InternalWidgets.wrapLateRenderable(InternalWidgets.mergeWidgets(
+        Widget tmp;
+        widgets.add(tmp = InternalWidgets.wrapLateRenderable(InternalWidgets.mergeWidgets(
                 Widgets.createButton(configButtonArea, NarratorManager.EMPTY)
                         .onClick(button -> {
                             if (Screen.hasShiftDown()) {
@@ -209,17 +224,8 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
                     helper.drawTexture(configButtonArea.x + 3, configButtonArea.y + 3, 0, 0, 14, 14);
                 })
                 )
-        )));
-        ((Widget) tmp).setZ(600);
-        lateRenderables.add(tmp);
-//        widgets.add((Widget) (tmp = InternalWidgets.wrapLateRenderable(Widgets.createTexturedWidget(CHEST_GUI_TEXTURE, configButtonArea.x + 3, configButtonArea.y + 3, 0, 0, 14, 14))));
-//        widgets.add((Widget) (tmp = InternalWidgets.wrapLateRenderable(Widgets.createDrawableWidget((helper, mouseX, mouseY, delta) -> {
-//            RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
-//            MinecraftClient.getInstance().getTextureManager().bindTexture(CHEST_GUI_TEXTURE);
-//            helper.blit(configButtonArea.x + 3, configButtonArea.y + 3, 0, 0, 14, 14);
-//        }))));
-//        ((Widget) tmp).setZ(600);
-//        lateRenderables.add(tmp);
+        ));
+        tmp.setZ(600);
         if (ConfigObject.getInstance().doesShowUtilsButtons()) {
             widgets.add(Widgets.createButton(ConfigObject.getInstance().isLowerConfigButton() ? new Rectangle(ConfigObject.getInstance().isLeftHandSidePanel() ? window.getScaledWidth() - 30 : 10, 10, 20, 20) : new Rectangle(ConfigObject.getInstance().isLeftHandSidePanel() ? window.getScaledWidth() - 55 : 35, 10, 20, 20), NarratorManager.EMPTY)
                     .onClick(button -> MinecraftClient.getInstance().player.sendChatMessage(ConfigObject.getInstance().getGamemodeCommand().replaceAll("\\{gamemode}", getNextGameMode(Screen.hasShiftDown()).getName())))
@@ -243,6 +249,20 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
                 xxx += ConfigObject.getInstance().isLeftHandSidePanel() ? -25 : 25;
             }
         }
+        subsetsButtonBounds = getSubsetsButtonBounds();
+        if (ConfigObject.getInstance().isSubsetsEnabled()) {
+            widgets.add(InternalWidgets.wrapLateRenderable(Widgets.createButton(subsetsButtonBounds, ((ClientHelperImpl) ClientHelper.getInstance()).isAprilFools.get() ? I18n.translate("text.rei.tiny_potato") : I18n.translate("text.rei.subsets"))
+                    .onClick(button -> {
+                        if (subsetsMenu == null) {
+                            wrappedSubsetsMenu = InternalWidgets.wrapTranslate(InternalWidgets.wrapLateRenderable(this.subsetsMenu = SubsetsMenu.createFromRegistry(new Point(this.subsetsButtonBounds.x, this.subsetsButtonBounds.getMaxY()))), 0, 0, 400);
+                            this.widgets.add(this.wrappedSubsetsMenu);
+                        } else {
+                            this.widgets.remove(this.wrappedSubsetsMenu);
+                            this.subsetsMenu = null;
+                            this.wrappedSubsetsMenu = null;
+                        }
+                    })));
+        }
         if (!ConfigObject.getInstance().isEntryListWidgetScrolled()) {
             widgets.add(Widgets.createClickableLabel(new Point(bounds.x + (bounds.width / 2), bounds.y + (ConfigObject.getInstance().getSearchFieldLocation() == SearchFieldLocation.TOP_SIDE ? 24 : 0) + 10), "", label -> {
                 ENTRY_LIST_WIDGET.setPage(0);
@@ -256,7 +276,7 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
             Rectangle area = getCraftableToggleArea();
             ItemRenderer itemRenderer = MinecraftClient.getInstance().getItemRenderer();
             ItemStack icon = new ItemStack(Blocks.CRAFTING_TABLE);
-            this.widgets.add((Widget) (tmp = InternalWidgets.wrapLateRenderable(InternalWidgets.mergeWidgets(
+            this.widgets.add(tmp = InternalWidgets.wrapLateRenderable(InternalWidgets.mergeWidgets(
                     Widgets.createButton(area, NarratorManager.EMPTY)
                             .focusable(false)
                             .onClick(button -> {
@@ -271,12 +291,27 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
                         itemRenderer.renderGuiItemIcon(icon, area.x + 2, area.y + 2);
                         itemRenderer.zOffset = 0.0F;
                     }))
-            )));
-            ((Widget) tmp).setZ(600);
-            lateRenderables.add(tmp);
+            ));
+            tmp.setZ(600);
         }
     }
     
+    @ApiStatus.Experimental
+    private Rectangle getSubsetsButtonBounds() {
+        if (ConfigObject.getInstance().isSubsetsEnabled()) {
+            if (MinecraftClient.getInstance().currentScreen instanceof RecipeViewingScreen) {
+                RecipeViewingScreen widget = (RecipeViewingScreen) MinecraftClient.getInstance().currentScreen;
+                return new Rectangle(widget.getBounds().x, 3, widget.getBounds().width, 18);
+            }
+            if (MinecraftClient.getInstance().currentScreen instanceof VillagerRecipeViewingScreen) {
+                VillagerRecipeViewingScreen widget = (VillagerRecipeViewingScreen) MinecraftClient.getInstance().currentScreen;
+                return new Rectangle(widget.bounds.x, 3, widget.bounds.width, 18);
+            }
+            return new Rectangle(((ContainerScreenHooks) ScreenHelper.getLastHandledScreen()).rei_getContainerLeft(), 3, ((ContainerScreenHooks) ScreenHelper.getLastHandledScreen()).rei_getContainerWidth(), 18);
+        }
+        return null;
+    }
+    
     private Weather getNextWeather() {
         try {
             Weather current = getCurrentWeather();
@@ -417,10 +452,15 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
     public void lateRender(int mouseX, int mouseY, float delta) {
         if (ScreenHelper.isOverlayVisible()) {
             ScreenHelper.getSearchField().laterRender(mouseX, mouseY, delta);
-            for (LateRenderable lateRenderable : lateRenderables) {
-                lateRenderable.lateRender(mouseX, mouseY, delta);
+            for (Widget widget : widgets) {
+                if (widget instanceof LateRenderable && wrappedSubsetsMenu != widget)
+                    widget.render(mouseX, mouseY, delta);
             }
         }
+        if (wrappedSubsetsMenu != null) {
+            TOOLTIPS.clear();
+            wrappedSubsetsMenu.render(mouseX, mouseY, delta);
+        }
         Screen currentScreen = MinecraftClient.getInstance().currentScreen;
         if (!(currentScreen instanceof RecipeViewingScreen) || !((RecipeViewingScreen) currentScreen).choosePageActivated)
             for (Tooltip tooltip : TOOLTIPS) {
@@ -462,7 +502,8 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
             rightButton.setEnabled(ENTRY_LIST_WIDGET.getTotalPages() > 1);
         }
         for (Widget widget : widgets) {
-            widget.render(int_1, int_2, float_1);
+            if (!(widget instanceof LateRenderable))
+                widget.render(int_1, int_2, float_1);
         }
     }
     
@@ -470,6 +511,8 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
     public boolean mouseScrolled(double i, double j, double amount) {
         if (!ScreenHelper.isOverlayVisible())
             return false;
+        if (wrappedSubsetsMenu != null && wrappedSubsetsMenu.mouseScrolled(i, j, amount))
+            return true;
         if (isInside(PointHelper.ofMouse())) {
             if (!ConfigObject.getInstance().isEntryListWidgetScrolled()) {
                 if (amount > 0 && leftButton.isEnabled())
@@ -487,7 +530,7 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
                 return true;
         }
         for (Widget widget : widgets)
-            if (widget != ENTRY_LIST_WIDGET && (favoritesListWidget == null || widget != favoritesListWidget) && widget.mouseScrolled(i, j, amount))
+            if (widget != ENTRY_LIST_WIDGET && (favoritesListWidget == null || widget != favoritesListWidget) && (wrappedSubsetsMenu == null || widget != wrappedSubsetsMenu) && widget.mouseScrolled(i, j, amount))
                 return true;
         return false;
     }
@@ -540,7 +583,7 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
     }
     
     @Override
-    public List<? extends Element> children() {
+    public List<Widget> children() {
         return widgets;
     }
     
@@ -548,6 +591,13 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
     public boolean mouseClicked(double double_1, double double_2, int int_1) {
         if (!ScreenHelper.isOverlayVisible())
             return false;
+        if (wrappedSubsetsMenu != null && wrappedSubsetsMenu.mouseClicked(double_1, double_2, int_1)) {
+            this.setFocused(wrappedSubsetsMenu);
+            if (int_1 == 0)
+                this.setDragging(true);
+            ScreenHelper.getSearchField().setFocused(false);
+            return true;
+        }
         if (MinecraftClient.getInstance().currentScreen instanceof HandledScreen && ConfigObject.getInstance().areClickableRecipeArrowsEnabled()) {
             ContainerScreenHooks hooks = (ContainerScreenHooks) MinecraftClient.getInstance().currentScreen;
             for (RecipeHelper.ScreenClickArea area : RecipeHelper.getInstance().getScreenClickAreas())
@@ -559,7 +609,7 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
                     }
         }
         for (Element element : widgets)
-            if (element.mouseClicked(double_1, double_2, int_1)) {
+            if (element != wrappedSubsetsMenu && element.mouseClicked(double_1, double_2, int_1)) {
                 this.setFocused(element);
                 if (int_1 == 0)
                     this.setDragging(true);

+ 30 - 102
src/main/java/me/shedaniel/rei/gui/VillagerRecipeViewingScreen.java

@@ -35,6 +35,7 @@ import me.shedaniel.rei.api.widgets.Button;
 import me.shedaniel.rei.api.widgets.Tooltip;
 import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.entries.RecipeEntry;
+import me.shedaniel.rei.gui.widget.ScrollingContainer;
 import me.shedaniel.rei.gui.widget.TabWidget;
 import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.impl.ClientHelperImpl;
@@ -44,9 +45,6 @@ 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.render.BufferBuilder;
-import net.minecraft.client.render.Tessellator;
-import net.minecraft.client.render.VertexFormats;
 import net.minecraft.client.resource.language.I18n;
 import net.minecraft.client.sound.PositionedSoundInstance;
 import net.minecraft.client.util.NarratorManager;
@@ -76,14 +74,24 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
     private int tabsPerPage = 8;
     private int selectedCategoryIndex = 0;
     private int selectedRecipeIndex = 0;
-    private double scrollAmount = 0;
-    private double target;
-    private long start;
-    private long duration;
+    private final ScrollingContainer scrolling = new ScrollingContainer() {
+        @Override
+        public Rectangle getBounds() {
+            return new Rectangle(scrollListBounds.x + 1, scrollListBounds.y + 1, scrollListBounds.width - 2, scrollListBounds.height - 2);
+        }
+        
+        @Override
+        public int getMaxScrollHeight() {
+            int i = 0;
+            for (Button button : buttonList) {
+                i += button.getBounds().height;
+            }
+            return i;
+        }
+    };
     private float scrollBarAlpha = 0;
     private float scrollBarAlphaFuture = 0;
     private long scrollBarAlphaFutureTime = -1;
-    private boolean draggingScrollBar = false;
     private int tabsPage = -1;
     private EntryStack ingredientStackToNotice = EntryStack.empty();
     private EntryStack resultStackToNotice = EntryStack.empty();
@@ -132,7 +140,7 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
         super.init();
         boolean isCompactTabs = ConfigObject.getInstance().isUsingCompactTabs();
         int tabSize = isCompactTabs ? 24 : 28;
-        this.draggingScrollBar = false;
+        scrolling.draggingScrollBar = false;
         this.children.clear();
         this.widgets.clear();
         this.buttonList.clear();
@@ -246,23 +254,12 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
         ScreenHelper.getLastOverlay().init();
     }
     
-    private double getMaxScroll() {
-        return Math.max(0, this.getMaxScrollPosition() - (scrollListBounds.height - 2));
-    }
-    
     @Override
     public boolean mouseClicked(double mouseX, double mouseY, int int_1) {
-        double height = getMaxScrollPosition();
-        int actualHeight = scrollListBounds.height - 2;
-        if (height > actualHeight && scrollBarAlpha > 0 && mouseY >= scrollListBounds.y + 1 && mouseY <= scrollListBounds.getMaxY() - 1) {
-            double scrollbarPositionMinX = scrollListBounds.getMaxX() - 6;
-            if (mouseX >= scrollbarPositionMinX & mouseX <= scrollbarPositionMinX + 8) {
-                this.draggingScrollBar = true;
-                scrollBarAlpha = 1;
-                return false;
-            }
+        if (scrolling.updateDraggingState(mouseX, mouseY, int_1)) {
+            scrollBarAlpha = 1;
+            return true;
         }
-        this.draggingScrollBar = false;
         return super.mouseClicked(mouseX, mouseY, int_1);
     }
     
@@ -274,29 +271,11 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
         return super.charTyped(char_1, int_1);
     }
     
-    public void offset(double value, boolean animated) {
-        scrollTo(target + value, animated);
-    }
-    
-    public void scrollTo(double value, boolean animated) {
-        scrollTo(value, animated, ClothConfigInitializer.getScrollDuration());
-    }
-    
-    public void scrollTo(double value, boolean animated, long duration) {
-        target = ClothConfigInitializer.clamp(value, getMaxScroll());
-        
-        if (animated) {
-            start = System.currentTimeMillis();
-            this.duration = duration;
-        } else
-            scrollAmount = target;
-    }
-    
     @Override
     public boolean mouseScrolled(double double_1, double double_2, double double_3) {
-        double height = CollectionUtils.sumInt(buttonList, b -> b.getBounds().getHeight());
+        double height = scrolling.getMaxScrollHeight();
         if (scrollListBounds.contains(double_1, double_2) && height > scrollListBounds.height - 2) {
-            offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
+            scrolling.offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
             if (scrollBarAlphaFuture == 0)
                 scrollBarAlphaFuture = 1f;
             if (System.currentTimeMillis() - scrollBarAlphaFutureTime > 300f)
@@ -323,10 +302,6 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
         return super.mouseScrolled(double_1, double_2, double_3);
     }
     
-    private double getMaxScrollPosition() {
-        return CollectionUtils.sumInt(buttonList, b -> b.getBounds().getHeight());
-    }
-    
     @Override
     public void render(int mouseX, int mouseY, float delta) {
         if (ConfigObject.getInstance().doesVillagerScreenHavePermanentScrollBar()) {
@@ -351,7 +326,7 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
                     scrollBarAlpha = Math.max(Math.min(1f, l / 300f), scrollBarAlpha);
             }
         }
-        updatePosition(delta);
+        scrolling.updatePosition(delta);
         this.fillGradient(0, 0, this.width, this.height, -1072689136, -804253680);
         int yOffset = 0;
         for (Widget widget : widgets) {
@@ -359,9 +334,9 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
         }
         ScreenHelper.getLastOverlay().render(mouseX, mouseY, delta);
         RenderSystem.pushMatrix();
-        ScissorsHandler.INSTANCE.scissor(new Rectangle(0, scrollListBounds.y + 1, width, scrollListBounds.height - 2));
+        ScissorsHandler.INSTANCE.scissor(scrolling.getBounds());
         for (Button button : buttonList) {
-            button.getBounds().y = scrollListBounds.y + 1 + yOffset - (int) scrollAmount;
+            button.getBounds().y = scrollListBounds.y + 1 + yOffset - (int) scrolling.scrollAmount;
             if (button.getBounds().getMaxY() > scrollListBounds.getMinY() && button.getBounds().getMinY() < scrollListBounds.getMaxY()) {
                 button.render(mouseX, mouseY, delta);
             }
@@ -374,65 +349,18 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
                 Optional.ofNullable(recipeRenderers.get(i).getTooltip(new Point(mouseX, mouseY))).ifPresent(Tooltip::queue);
             }
         }
-        double maxScroll = getMaxScrollPosition();
-        if (maxScroll > scrollListBounds.height - 2) {
-            Tessellator tessellator = Tessellator.getInstance();
-            BufferBuilder buffer = tessellator.getBuffer();
-            int height = (int) (((scrollListBounds.height - 2) * (scrollListBounds.height - 2)) / this.getMaxScrollPosition());
-            height = MathHelper.clamp(height, 32, scrollListBounds.height - 2 - 8);
-            height -= Math.min((scrollAmount < 0 ? (int) -scrollAmount : scrollAmount > getMaxScroll() ? (int) scrollAmount - getMaxScroll() : 0), height * .95);
-            height = Math.max(10, height);
-            int minY = (int) Math.min(Math.max((int) scrollAmount * (scrollListBounds.height - 2 - height) / getMaxScroll() + scrollListBounds.y + 1, scrollListBounds.y + 1), scrollListBounds.getMaxY() - 1 - height);
-            int scrollbarPositionMinX = scrollListBounds.getMaxX() - 6, scrollbarPositionMaxX = scrollListBounds.getMaxX() - 1;
-            boolean hovered = (new Rectangle(scrollbarPositionMinX, minY, scrollbarPositionMaxX - scrollbarPositionMinX, height)).contains(PointHelper.ofMouse());
-            float bottomC = (hovered ? .67f : .5f) * (REIHelper.getInstance().isDarkThemeEnabled() ? 0.8f : 1f);
-            float topC = (hovered ? .87f : .67f) * (REIHelper.getInstance().isDarkThemeEnabled() ? 0.8f : 1f);
-            RenderSystem.disableTexture();
-            RenderSystem.enableBlend();
-            RenderSystem.disableAlphaTest();
-            RenderSystem.blendFuncSeparate(770, 771, 1, 0);
-            RenderSystem.shadeModel(7425);
-            buffer.begin(7, VertexFormats.POSITION_COLOR);
-            buffer.vertex(scrollbarPositionMinX, minY + height, 800).color(bottomC, bottomC, bottomC, scrollBarAlpha).next();
-            buffer.vertex(scrollbarPositionMaxX, minY + height, 800).color(bottomC, bottomC, bottomC, scrollBarAlpha).next();
-            buffer.vertex(scrollbarPositionMaxX, minY, 800).color(bottomC, bottomC, bottomC, scrollBarAlpha).next();
-            buffer.vertex(scrollbarPositionMinX, minY, 800).color(bottomC, bottomC, bottomC, scrollBarAlpha).next();
-            tessellator.draw();
-            buffer.begin(7, VertexFormats.POSITION_COLOR);
-            buffer.vertex(scrollbarPositionMinX, minY + height - 1, 800).color(topC, topC, topC, scrollBarAlpha).next();
-            buffer.vertex(scrollbarPositionMaxX - 1, minY + height - 1, 800).color(topC, topC, topC, scrollBarAlpha).next();
-            buffer.vertex(scrollbarPositionMaxX - 1, minY, 800).color(topC, topC, topC, scrollBarAlpha).next();
-            buffer.vertex(scrollbarPositionMinX, minY, 800).color(topC, topC, topC, scrollBarAlpha).next();
-            tessellator.draw();
-            RenderSystem.shadeModel(7424);
-            RenderSystem.disableBlend();
-            RenderSystem.enableAlphaTest();
-            RenderSystem.enableTexture();
-        }
+        scrolling.renderScrollBar(0, scrollBarAlpha);
         ScissorsHandler.INSTANCE.removeLastScissor();
         RenderSystem.popMatrix();
         ScreenHelper.getLastOverlay().lateRender(mouseX, mouseY, delta);
     }
     
-    private void updatePosition(float delta) {
-        double[] target = new double[]{this.target};
-        this.scrollAmount = ClothConfigInitializer.handleScrollingPosition(target, this.scrollAmount, this.getMaxScroll(), delta, this.start, this.duration);
-        this.target = target[0];
-    }
-    
     @Override
     public boolean mouseDragged(double mouseX, double mouseY, int int_1, double double_3, double double_4) {
-        if (int_1 == 0 && scrollBarAlpha > 0 && draggingScrollBar) {
-            double height = CollectionUtils.sumInt(buttonList, b -> b.getBounds().getHeight());
-            int actualHeight = scrollListBounds.height - 2;
-            if (height > actualHeight && mouseY >= scrollListBounds.y + 1 && mouseY <= scrollListBounds.getMaxY() - 1) {
-                int int_3 = MathHelper.clamp((int) ((actualHeight * actualHeight) / height), 32, actualHeight - 8);
-                double double_6 = Math.max(1.0D, Math.max(1d, height) / (double) (actualHeight - int_3));
-                scrollBarAlphaFutureTime = System.currentTimeMillis();
-                scrollBarAlphaFuture = 1f;
-                scrollAmount = target = MathHelper.clamp(scrollAmount + double_4 * double_6, 0, height - scrollListBounds.height + 2);
-                return true;
-            }
+        if (scrolling.mouseDragged(mouseX, mouseY, int_1, double_3, double_4)) {
+            scrollBarAlphaFutureTime = System.currentTimeMillis();
+            scrollBarAlphaFuture = 1f;
+            return true;
         }
         return super.mouseDragged(mouseX, mouseY, int_1, double_3, double_4);
     }

+ 39 - 136
src/main/java/me/shedaniel/rei/gui/config/entry/FilteringEntry.java

@@ -36,10 +36,10 @@ 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.gui.widget.ScrollingContainer;
 import me.shedaniel.rei.impl.ScreenHelper;
 import me.shedaniel.rei.impl.SearchArgument;
 import me.shedaniel.rei.utils.CollectionUtils;
@@ -47,9 +47,6 @@ 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.resource.language.I18n;
 import net.minecraft.util.math.MathHelper;
 import org.jetbrains.annotations.ApiStatus;
@@ -66,10 +63,22 @@ 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 double target;
-    protected double scroll;
-    protected long start;
-    protected long duration;
+    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;
@@ -79,7 +88,6 @@ public class FilteringEntry extends AbstractConfigListEntry<List<EntryStack>> {
     private Rectangle innerBounds;
     private List<EntryListEntry> entries = Collections.emptyList();
     private List<Element> elements = Collections.emptyList();
-    private boolean draggingScrollBar = false;
     
     private Point selectionPoint = null;
     private Point secondPoint = null;
@@ -118,7 +126,7 @@ public class FilteringEntry extends AbstractConfigListEntry<List<EntryStack>> {
                 for (int i = 0; i < entryStacks.size(); i++) {
                     EntryStack stack = entryStacks.get(i);
                     EntryListEntry entry = entries.get(i);
-                    entry.getBounds().y = (int) (entry.backupY - scroll);
+                    entry.getBounds().y = (int) (entry.backupY - scrolling.scrollAmount);
                     if (entry.isSelected() && !entry.isFiltered()) {
                         configFiltered.add(stack);
                         getScreen().setEdited(true, false);
@@ -132,7 +140,7 @@ public class FilteringEntry extends AbstractConfigListEntry<List<EntryStack>> {
                 for (int i = 0; i < entryStacks.size(); i++) {
                     EntryStack stack = entryStacks.get(i);
                     EntryListEntry entry = entries.get(i);
-                    entry.getBounds().y = (int) (entry.backupY - scroll);
+                    entry.getBounds().y = (int) (entry.backupY - scrolling.scrollAmount);
                     if (entry.isSelected() && configFiltered.remove(stack)) {
                         getScreen().setEdited(true, false);
                     }
@@ -192,13 +200,13 @@ public class FilteringEntry extends AbstractConfigListEntry<List<EntryStack>> {
             return;
         for (EntryListEntry entry : entries)
             entry.clearStacks();
-        int skip = Math.max(0, MathHelper.floor(scroll / (float) entrySize()));
+        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 - scroll);
+            entry.getBounds().y = (int) (entry.backupY - scrolling.scrollAmount);
             if (entry.getBounds().y > bounds.getMaxY())
                 break;
             entry.entry(stack);
@@ -206,7 +214,7 @@ public class FilteringEntry extends AbstractConfigListEntry<List<EntryStack>> {
             nextIndex++;
         }
         updatePosition(delta);
-        renderScrollbar();
+        scrolling.renderScrollBar(0xff000000, 1);
         RenderSystem.translatef(0, 0, 300);
         this.searchField.laterRender(mouseX, mouseY, delta);
         this.selectAllButton.render(mouseX, mouseY, delta);
@@ -224,95 +232,33 @@ public class FilteringEntry extends AbstractConfigListEntry<List<EntryStack>> {
             Point p = secondPoint;
             if (p == null) {
                 p = PointHelper.ofMouse();
-                p.translate(0, (int) scroll);
+                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 - scroll), right - left, bottom - top);
+            return new Rectangle(left, (int) (top - scrolling.scrollAmount), right - left, bottom - top);
         }
         return new Rectangle(0, 0, 0, 0);
     }
     
-    private int getScrollbarMinX() {
-        return getParent().right - 7;
-    }
-    
     @Override
-    public boolean mouseDragged(double mouseX, double mouseY, int int_1, double double_3, double double_4) {
-        if (int_1 == 0 && draggingScrollBar) {
-            float height = getMaxScrollPosition();
-            int actualHeight = innerBounds.height;
-            if (height > actualHeight && mouseY >= innerBounds.y && mouseY <= innerBounds.getMaxY()) {
-                double double_5 = Math.max(1, this.getMaxScroll());
-                int int_2 = innerBounds.height;
-                int int_3 = MathHelper.clamp((int) ((float) (int_2 * int_2) / (float) getMaxScrollPosition()), 32, int_2 - 8);
-                double double_6 = Math.max(1.0D, double_5 / (double) (int_2 - int_3));
-                float to = MathHelper.clamp((float) (scroll + double_4 * double_6), 0, height - innerBounds.height);
-                if (ConfigObject.getInstance().doesSnapToRows()) {
-                    double nearestRow = Math.round(to / (double) entrySize()) * (double) entrySize();
-                    scrollTo(nearestRow, false);
-                } else
-                    scrollTo(to, false);
-            }
+    public boolean mouseDragged(double mouseX, double mouseY, int button, double dx, double dy) {
+        if (scrolling.mouseDragged(mouseX, mouseY, button, dx, dy, true))
             return true;
-        }
-        return super.mouseDragged(mouseX, mouseY, int_1, double_3, double_4);
-    }
-    
-    private void renderScrollbar() {
-        int maxScroll = getMaxScroll();
-        if (maxScroll > 0) {
-            int height = innerBounds.height * innerBounds.height / getMaxScrollPosition();
-            height = MathHelper.clamp(height, 32, innerBounds.height - 8);
-            height -= Math.min((scroll < 0 ? (int) -scroll : scroll > maxScroll ? (int) scroll - maxScroll : 0), height * .95);
-            height = Math.max(10, height);
-            int minY = Math.min(Math.max((int) scroll * (innerBounds.height - height) / maxScroll + innerBounds.y, innerBounds.y), innerBounds.getMaxY() - height);
-            
-            int scrollbarPositionMinX = getScrollbarMinX();
-            int scrollbarPositionMaxX = scrollbarPositionMinX + 6;
-            boolean hovered = (new Rectangle(scrollbarPositionMinX, minY, scrollbarPositionMaxX - scrollbarPositionMinX, height)).contains(PointHelper.ofMouse());
-            float bottomC = (hovered ? .67f : .5f) * (REIHelper.getInstance().isDarkThemeEnabled() ? 0.8f : 1f);
-            float topC = (hovered ? .87f : .67f) * (REIHelper.getInstance().isDarkThemeEnabled() ? 0.8f : 1f);
-            
-            RenderSystem.disableTexture();
-            RenderSystem.enableBlend();
-            RenderSystem.disableAlphaTest();
-            RenderSystem.blendFuncSeparate(770, 771, 1, 0);
-            RenderSystem.shadeModel(7425);
-            Tessellator tessellator = Tessellator.getInstance();
-            BufferBuilder buffer = tessellator.getBuffer();
-            buffer.begin(7, VertexFormats.POSITION_COLOR);
-            buffer.vertex(scrollbarPositionMinX, minY + height, 0.0D).color(bottomC, bottomC, bottomC, 1).next();
-            buffer.vertex(scrollbarPositionMaxX, minY + height, 0.0D).color(bottomC, bottomC, bottomC, 1).next();
-            buffer.vertex(scrollbarPositionMaxX, minY, 0.0D).color(bottomC, bottomC, bottomC, 1).next();
-            buffer.vertex(scrollbarPositionMinX, minY, 0.0D).color(bottomC, bottomC, bottomC, 1).next();
-            tessellator.draw();
-            buffer.begin(7, VertexFormats.POSITION_COLOR);
-            buffer.vertex(scrollbarPositionMinX, (minY + height - 1), 0.0D).color(topC, topC, topC, 1).next();
-            buffer.vertex((scrollbarPositionMaxX - 1), (minY + height - 1), 0.0D).color(topC, topC, topC, 1).next();
-            buffer.vertex((scrollbarPositionMaxX - 1), minY, 0.0D).color(topC, topC, topC, 1).next();
-            buffer.vertex(scrollbarPositionMinX, minY, 0.0D).color(topC, topC, topC, 1).next();
-            tessellator.draw();
-            RenderSystem.shadeModel(7424);
-            RenderSystem.disableBlend();
-            RenderSystem.enableAlphaTest();
-            RenderSystem.enableTexture();
-        }
+        return super.mouseDragged(mouseX, mouseY, button, dx, dy);
     }
     
     private void updatePosition(float delta) {
-        if (ConfigObject.getInstance().doesSnapToRows() && target >= 0 && target <= getMaxScroll()) {
-            double nearestRow = Math.round(target / (double) entrySize()) * (double) entrySize();
-            if (!DynamicNewSmoothScrollingEntryListWidget.Precision.almostEquals(target, nearestRow, DynamicNewSmoothScrollingEntryListWidget.Precision.FLOAT_EPSILON))
-                target += (nearestRow - target) * Math.min(delta / 2.0, 1.0);
+        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
-                target = nearestRow;
+                scrolling.scrollTarget = nearestRow;
         }
-        double[] targetD = new double[]{this.target};
-        this.scroll = ClothConfigInitializer.handleScrollingPosition(targetD, this.scroll, this.getMaxScroll(), delta, this.start, this.duration);
-        this.target = targetD[0];
+        scrolling.updatePosition(delta);
     }
     
     public void updateSearch(String searchTerm) {
@@ -362,19 +308,10 @@ public class FilteringEntry extends AbstractConfigListEntry<List<EntryStack>> {
     
     @Override
     public boolean mouseClicked(double double_1, double double_2, int int_1) {
-        double height = getMaxScroll();
-        Rectangle bounds = getBounds();
-        int actualHeight = bounds.height;
-        if (height > actualHeight && double_2 >= bounds.y && double_2 <= bounds.getMaxY()) {
-            double scrollbarPositionMinX = getScrollbarMinX();
-            if (double_1 >= scrollbarPositionMinX - 1 & double_1 <= scrollbarPositionMinX + 8) {
-                this.draggingScrollBar = true;
-                return true;
-            }
-        }
-        this.draggingScrollBar = false;
+        if (scrolling.updateDraggingState(double_1, double_2, int_1))
+            return true;
         
-        if (bounds.contains(double_1, double_2)) {
+        if (getBounds().contains(double_1, double_2)) {
             if (searchField.mouseClicked(double_1, double_2, int_1)) {
                 this.selectionPoint = null;
                 this.secondPoint = null;
@@ -389,7 +326,7 @@ public class FilteringEntry extends AbstractConfigListEntry<List<EntryStack>> {
                 return true;
             }
             if (int_1 == 0) {
-                this.selectionPoint = new Point(double_1, double_2 + scroll);
+                this.selectionPoint = new Point(double_1, double_2 + scrolling.scrollAmount);
                 this.secondPoint = null;
                 return true;
             }
@@ -400,7 +337,7 @@ public class FilteringEntry extends AbstractConfigListEntry<List<EntryStack>> {
     @Override
     public boolean mouseReleased(double mouseX, double mouseY, int button) {
         if (selectionPoint != null && button == 0 && secondPoint == null) {
-            this.secondPoint = new Point(mouseX, mouseY + scroll);
+            this.secondPoint = new Point(mouseX, mouseY + scrolling.scrollAmount);
             if (secondPoint.equals(selectionPoint)) {
                 secondPoint.translate(1, 1);
             }
@@ -439,44 +376,10 @@ public class FilteringEntry extends AbstractConfigListEntry<List<EntryStack>> {
             updateEntriesPosition();
     }
     
-    protected final int getMaxScrollPosition() {
-        return MathHelper.ceil(entryStacks.size() / (innerBounds.width / (float) entrySize())) * entrySize() + 28;
-    }
-    
-    protected final int getMaxScroll() {
-        return Math.max(0, this.getMaxScrollPosition() - innerBounds.height);
-    }
-    
-    protected final double clamp(double v) {
-        return this.clamp(v, 200.0D);
-    }
-    
-    protected final double clamp(double v, double clampExtension) {
-        return MathHelper.clamp(v, -clampExtension, (double) this.getMaxScroll() + clampExtension);
-    }
-    
-    protected final void offset(double value, boolean animated) {
-        scrollTo(target + value, animated);
-    }
-    
-    protected final void scrollTo(double value, boolean animated) {
-        scrollTo(value, animated, ClothConfigInitializer.getScrollDuration());
-    }
-    
-    protected final void scrollTo(double value, boolean animated, long duration) {
-        target = clamp(value);
-        
-        if (animated) {
-            start = System.currentTimeMillis();
-            this.duration = duration;
-        } else
-            scroll = target;
-    }
-    
     @Override
     public boolean mouseScrolled(double double_1, double double_2, double double_3) {
         if (getBounds().contains(double_1, double_2)) {
-            offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
+            scrolling.offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
             return true;
         }
         super.mouseScrolled(double_1, double_2, double_3);

+ 256 - 0
src/main/java/me/shedaniel/rei/gui/subsets/SubsetsMenu.java

@@ -0,0 +1,256 @@
+package me.shedaniel.rei.gui.subsets;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import me.shedaniel.clothconfig2.ClothConfigInitializer;
+import me.shedaniel.clothconfig2.api.ScissorsHandler;
+import me.shedaniel.math.Point;
+import me.shedaniel.math.Rectangle;
+import me.shedaniel.rei.RoughlyEnoughItemsCore;
+import me.shedaniel.rei.api.EntryRegistry;
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.api.subsets.SubsetsRegistry;
+import me.shedaniel.rei.gui.subsets.entries.EntryStackMenuEntry;
+import me.shedaniel.rei.gui.subsets.entries.SubMenuEntry;
+import me.shedaniel.rei.gui.widget.LateRenderable;
+import me.shedaniel.rei.gui.widget.ScrollingContainer;
+import me.shedaniel.rei.gui.widget.WidgetWithBounds;
+import me.shedaniel.rei.impl.EntryRegistryImpl;
+import me.shedaniel.rei.utils.CollectionUtils;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.item.Item;
+import net.minecraft.item.ItemGroup;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.collection.DefaultedList;
+import net.minecraft.util.registry.Registry;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.*;
+
+@ApiStatus.Experimental
+@ApiStatus.Internal
+public class SubsetsMenu extends WidgetWithBounds implements LateRenderable {
+    public final Point menuStartPoint;
+    private final List<SubsetsMenuEntry> entries = Lists.newArrayList();
+    public final ScrollingContainer scrolling = new ScrollingContainer() {
+        @Override
+        public int getMaxScrollHeight() {
+            int i = 0;
+            for (SubsetsMenuEntry entry : children()) {
+                i += entry.getEntryHeight();
+            }
+            return i;
+        }
+        
+        @Override
+        public Rectangle getBounds() {
+            return SubsetsMenu.this.getInnerBounds();
+        }
+        
+        @Override
+        public boolean hasScrollBar() {
+            return SubsetsMenu.this.hasScrollBar();
+        }
+    };
+    
+    public SubsetsMenu(Point menuStartPoint, Collection<SubsetsMenuEntry> entries) {
+        this.menuStartPoint = menuStartPoint;
+        buildEntries(entries);
+    }
+    
+    public static SubsetsMenu createFromRegistry(Point menuStartPoint) {
+        List<EntryStack> stacks = EntryRegistry.getInstance().getStacksList();
+        Map<String, Object> entries = Maps.newHashMap();
+        {
+            // All Entries group
+            Map<String, Object> allEntries = getOrCreateSubEntryInMap(entries, "roughlyenoughitems:all_entries");
+            for (EntryStack stack : stacks) {
+                putEntryInMap(allEntries, stack);
+            }
+        }
+        {
+            // Item Groups group
+            Map<String, Object> itemGroups = getOrCreateSubEntryInMap(entries, "roughlyenoughitems:item_groups");
+            for (Item item : Registry.ITEM) {
+                ItemGroup group = item.getGroup();
+                if (group == null)
+                    continue;
+                DefaultedList<ItemStack> list;
+                try {
+                    list = new EntryRegistryImpl.DefaultedLinkedList<>(Lists.newLinkedList(), null);
+                    item.appendStacks(group, list);
+                    if (list.isEmpty())
+                        list.add(item.getStackForRender());
+                    Map<String, Object> groupMenu = getOrCreateSubEntryInMap(itemGroups, "_item_group_" + group.getId());
+                    for (ItemStack stack : list) {
+                        putEntryInMap(groupMenu, EntryStack.create(stack));
+                    }
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+        Set<String> paths = SubsetsRegistry.INSTANCE.getPaths();
+        for (String path : paths) {
+            Map<String, Object> lastMap = entries;
+            String[] pathSegments = path.split("/");
+            for (String pathSegment : pathSegments) {
+                lastMap = getOrCreateSubEntryInMap(lastMap, pathSegment);
+            }
+            for (EntryStack entry : SubsetsRegistry.INSTANCE.getPathEntries(path)) {
+                EntryStack firstStack = CollectionUtils.findFirstOrNullEqualsEntryIgnoreAmount(stacks, entry);
+                if (firstStack != null)
+                    putEntryInMap(lastMap, firstStack);
+            }
+        }
+        return new SubsetsMenu(menuStartPoint, buildEntries(entries));
+    }
+    
+    private static Map<String, Object> getOrCreateSubEntryInMap(Map<String, Object> parent, String pathSegment) {
+        putEntryInMap(parent, pathSegment);
+        return (Map<String, Object>) parent.get(pathSegment);
+    }
+    
+    private static void putEntryInMap(Map<String, Object> parent, String pathSegment) {
+        if (!parent.containsKey(pathSegment)) {
+            parent.put(pathSegment, Maps.newHashMap());
+        }
+    }
+    
+    private static void putEntryInMap(Map<String, Object> parent, EntryStack stack) {
+        Set<EntryStack> items = (Set<EntryStack>) parent.get("items");
+        if (items == null) {
+            items = Sets.newLinkedHashSet();
+            parent.put("items", items);
+        }
+        items.add(stack);
+    }
+    
+    private static List<SubsetsMenuEntry> buildEntries(Map<String, Object> map) {
+        List<SubsetsMenuEntry> entries = Lists.newArrayList();
+        for (Map.Entry<String, Object> entry : map.entrySet()) {
+            if (entry.getKey().equals("items")) {
+                Set<EntryStack> items = (Set<EntryStack>) entry.getValue();
+                for (EntryStack item : items) {
+                    entries.add(new EntryStackMenuEntry(item));
+                }
+            } else {
+                Map<String, Object> entryMap = (Map<String, Object>) entry.getValue();
+                if (entry.getKey().startsWith("_item_group_")) {
+                    entries.add(new SubMenuEntry(I18n.translate(entry.getKey().replace("_item_group_", "itemGroup.")), buildEntries(entryMap)));
+                } else {
+                    String translationKey = "subsets.rei." + entry.getKey().replace(':', '.');
+                    if (!I18n.hasTranslation(translationKey))
+                        RoughlyEnoughItemsCore.LOGGER.warn("[REI] Subsets menu " + translationKey + " does not have a translation");
+                    entries.add(new SubMenuEntry(I18n.translate(translationKey), buildEntries(entryMap)));
+                }
+            }
+        }
+        return entries;
+    }
+    
+    @SuppressWarnings("deprecation")
+    private void buildEntries(Collection<SubsetsMenuEntry> entries) {
+        this.entries.clear();
+        this.entries.addAll(entries);
+        this.entries.sort(Comparator.comparing(entry -> entry instanceof SubMenuEntry ? 0 : 1).thenComparing(entry -> entry instanceof SubMenuEntry ? ((SubMenuEntry) entry).text : ""));
+        for (SubsetsMenuEntry entry : this.entries) {
+            entry.parent = this;
+        }
+    }
+    
+    @Override
+    public @NotNull Rectangle getBounds() {
+        return new Rectangle(menuStartPoint.x, menuStartPoint.y, getMaxEntryWidth() + 2 + (hasScrollBar() ? 6 : 0), getInnerHeight() + 2);
+    }
+    
+    public Rectangle getInnerBounds() {
+        return new Rectangle(menuStartPoint.x + 1, menuStartPoint.y + 1, getMaxEntryWidth() + (hasScrollBar() ? 6 : 0), getInnerHeight());
+    }
+    
+    public boolean hasScrollBar() {
+        return scrolling.getMaxScrollHeight() > getInnerHeight();
+    }
+    
+    public int getInnerHeight() {
+        return Math.min(scrolling.getMaxScrollHeight(), minecraft.currentScreen.height - 20 - menuStartPoint.y);
+    }
+    
+    public int getMaxEntryWidth() {
+        int i = 0;
+        for (SubsetsMenuEntry entry : children()) {
+            if (entry.getEntryWidth() > i)
+                i = entry.getEntryWidth();
+        }
+        return Math.max(10, i);
+    }
+    
+    @Override
+    public void render(int mouseX, int mouseY, float delta) {
+        Rectangle bounds = getBounds();
+        Rectangle innerBounds = getInnerBounds();
+        fill(bounds.x, bounds.y, bounds.getMaxX(), bounds.getMaxY(), -6250336);
+        fill(innerBounds.x, innerBounds.y, innerBounds.getMaxX(), innerBounds.getMaxY(), -16777216);
+        boolean contains = innerBounds.contains(mouseX, mouseY);
+        SubsetsMenuEntry focused = getFocused() instanceof SubsetsMenuEntry ? (SubsetsMenuEntry) getFocused() : null;
+        int currentY = (int) (innerBounds.y - scrolling.scrollAmount);
+        for (SubsetsMenuEntry child : children()) {
+            boolean containsMouse = contains && mouseY >= currentY && mouseY < currentY + child.getEntryHeight();
+            if (containsMouse) {
+                focused = child;
+            }
+            currentY += child.getEntryHeight();
+        }
+        currentY = (int) (innerBounds.y - scrolling.scrollAmount);
+        ScissorsHandler.INSTANCE.scissor(scrolling.getScissorBounds());
+        for (SubsetsMenuEntry child : children()) {
+            boolean rendering = currentY + child.getEntryHeight() >= innerBounds.y && currentY <= innerBounds.getMaxY();
+            boolean containsMouse = contains && mouseY >= currentY && mouseY < currentY + child.getEntryHeight();
+            child.updateInformation(innerBounds.x, currentY, focused == child || containsMouse, containsMouse, rendering, getMaxEntryWidth());
+            if (rendering)
+                child.render(mouseX, mouseY, delta);
+            currentY += child.getEntryHeight();
+        }
+        ScissorsHandler.INSTANCE.removeLastScissor();
+        setFocused(focused);
+        scrolling.renderScrollBar();
+        scrolling.updatePosition(delta);
+    }
+    
+    @Override
+    public boolean mouseClicked(double mouseX, double mouseY, int button) {
+        if (scrolling.updateDraggingState(mouseX, mouseY, button))
+            return true;
+        return super.mouseClicked(mouseX, mouseY, button);
+    }
+    
+    @Override
+    public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
+        if (scrolling.mouseDragged(mouseX, mouseY, button, deltaX, deltaY))
+            return true;
+        return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY);
+    }
+    
+    @Override
+    public boolean mouseScrolled(double mouseX, double mouseY, double amount) {
+        if (getInnerBounds().contains(mouseX, mouseY)) {
+            scrolling.offset(ClothConfigInitializer.getScrollStep() * -amount, true);
+            return true;
+        }
+        for (SubsetsMenuEntry child : children()) {
+            if (child instanceof SubMenuEntry) {
+                if (child.mouseScrolled(mouseX, mouseY, amount))
+                    return true;
+            }
+        }
+        return super.mouseScrolled(mouseX, mouseY, amount);
+    }
+    
+    @Override
+    public List<SubsetsMenuEntry> children() {
+        return entries;
+    }
+}

+ 21 - 0
src/main/java/me/shedaniel/rei/gui/subsets/SubsetsMenuEntry.java

@@ -0,0 +1,21 @@
+package me.shedaniel.rei.gui.subsets;
+
+import me.shedaniel.rei.gui.widget.Widget;
+import org.jetbrains.annotations.ApiStatus;
+
+@ApiStatus.Experimental
+@ApiStatus.Internal
+public abstract class SubsetsMenuEntry extends Widget {
+    @Deprecated
+    SubsetsMenu parent = null;
+    
+    public final SubsetsMenu getParent() {
+        return parent;
+    }
+    
+    public abstract int getEntryWidth();
+    
+    public abstract int getEntryHeight();
+    
+    public abstract void updateInformation(int xPos, int yPos, boolean selected, boolean containsMouse, boolean rendering, int width);
+}

+ 117 - 0
src/main/java/me/shedaniel/rei/gui/subsets/entries/EntryStackMenuEntry.java

@@ -0,0 +1,117 @@
+package me.shedaniel.rei.gui.subsets.entries;
+
+import me.shedaniel.math.Point;
+import me.shedaniel.math.Rectangle;
+import me.shedaniel.rei.RoughlyEnoughItemsCore;
+import me.shedaniel.rei.api.*;
+import me.shedaniel.rei.gui.ContainerScreenOverlay;
+import me.shedaniel.rei.gui.subsets.SubsetsMenu;
+import me.shedaniel.rei.gui.subsets.SubsetsMenuEntry;
+import me.shedaniel.rei.gui.widget.Widget;
+import me.shedaniel.rei.impl.EntryRegistryImpl;
+import me.shedaniel.rei.impl.ScreenHelper;
+import me.shedaniel.rei.utils.CollectionUtils;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.sound.PositionedSoundInstance;
+import net.minecraft.sound.SoundEvents;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.Collections;
+import java.util.List;
+
+@ApiStatus.Experimental
+@ApiStatus.Internal
+public class EntryStackMenuEntry extends SubsetsMenuEntry {
+    final EntryStack stack;
+    private int x, y, width;
+    private boolean selected, containsMouse, rendering;
+    private boolean clickedLast = false;
+    private Boolean isFiltered = null;
+    
+    public EntryStackMenuEntry(EntryStack stack) {
+        this.stack = stack;
+    }
+    
+    @Override
+    public int getEntryWidth() {
+        return 18;
+    }
+    
+    @Override
+    public int getEntryHeight() {
+        return 18;
+    }
+    
+    @Override
+    public void updateInformation(int xPos, int yPos, boolean selected, boolean containsMouse, boolean rendering, int width) {
+        this.x = xPos;
+        this.y = yPos;
+        this.selected = selected;
+        this.containsMouse = containsMouse;
+        this.rendering = rendering;
+        this.width = width;
+    }
+    
+    @Override
+    public void render(int mouseX, int mouseY, float delta) {
+        if (isFiltered()) {
+            if (selected) {
+                fill(x, y, x + width, y + 18, -26215);
+            } else {
+                fill(x, y, x + width, y + 18, -65536);
+            }
+        } else if (selected) {
+            fill(x, y, x + width, y + 18, 1174405119);
+        }
+        if (containsMouse && mouseX >= x + (width / 2) - 8 && mouseX <= x + (width / 2) + 8 && mouseY >= y + 1 && mouseY <= y + 17) {
+            REIHelper.getInstance().queueTooltip(stack.getTooltip(new Point(mouseX, mouseY)));
+            if (RoughlyEnoughItemsCore.isLeftModePressed && !clickedLast) {
+                clickedLast = true;
+                if (!getParent().scrolling.draggingScrollBar) {
+                    minecraft.getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
+                    List<EntryStack> filteredStacks = ConfigObject.getInstance().getFilteredStacks();
+                    if (isFiltered()) {
+                        filteredStacks.removeIf(next -> next.equalsIgnoreAmount(stack));
+                    } else {
+                        filteredStacks.add(stack.copy());
+                    }
+                    SubsetsMenu subsetsMenu = ScreenHelper.getLastOverlay().getSubsetsMenu();
+                    if (subsetsMenu != null)
+                        recalculateFilter(subsetsMenu);
+                    ConfigManager.getInstance().saveConfig();
+                    ((EntryRegistryImpl) EntryRegistry.getInstance()).refilter();
+                    if (ScreenHelper.getSearchField() != null)
+                        ContainerScreenOverlay.getEntryListWidget().updateSearch(ScreenHelper.getSearchField().getText(), true);
+                }
+            } else if (!RoughlyEnoughItemsCore.isLeftModePressed) clickedLast = false;
+        } else clickedLast = false;
+        stack.render(new Rectangle(x + (width / 2) - 8, y + 1, 16, 16), mouseX, mouseY, delta);
+    }
+    
+    void recalculateFilter(SubsetsMenu menu) {
+        for (SubsetsMenuEntry child : menu.children()) {
+            if (child instanceof SubMenuEntry && ((SubMenuEntry) child).getSubsetsMenu() != null)
+                recalculateFilter(((SubMenuEntry) child).getSubsetsMenu());
+            else if (child instanceof EntryStackMenuEntry && ((EntryStackMenuEntry) child).stack.equalsIgnoreAmount(stack))
+                ((EntryStackMenuEntry) child).isFiltered = null;
+        }
+    }
+    
+    @Override
+    public boolean mouseClicked(double mouseX, double mouseY, int button) {
+        return rendering && mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + 18;
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        return Collections.emptyList();
+    }
+    
+    public boolean isFiltered() {
+        List<EntryStack> filteredStacks = ConfigObject.getInstance().getFilteredStacks();
+        if (isFiltered == null) {
+            isFiltered = CollectionUtils.findFirstOrNullEqualsEntryIgnoreAmount(filteredStacks, stack) != null;
+        }
+        return isFiltered;
+    }
+}

+ 209 - 0
src/main/java/me/shedaniel/rei/gui/subsets/entries/SubMenuEntry.java

@@ -0,0 +1,209 @@
+package me.shedaniel.rei.gui.subsets.entries;
+
+import com.google.common.collect.Lists;
+import me.shedaniel.clothconfig2.api.ScissorsHandler;
+import me.shedaniel.math.Point;
+import me.shedaniel.math.Rectangle;
+import me.shedaniel.rei.api.*;
+import me.shedaniel.rei.api.widgets.Tooltip;
+import me.shedaniel.rei.gui.ContainerScreenOverlay;
+import me.shedaniel.rei.gui.subsets.SubsetsMenu;
+import me.shedaniel.rei.gui.subsets.SubsetsMenuEntry;
+import me.shedaniel.rei.gui.widget.TabWidget;
+import me.shedaniel.rei.impl.EntryRegistryImpl;
+import me.shedaniel.rei.impl.ScreenHelper;
+import me.shedaniel.rei.utils.CollectionUtils;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.sound.PositionedSoundInstance;
+import net.minecraft.sound.SoundEvents;
+import net.minecraft.util.Pair;
+import net.minecraft.util.math.MathHelper;
+import org.jetbrains.annotations.ApiStatus;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Supplier;
+
+@ApiStatus.Experimental
+@ApiStatus.Internal
+public class SubMenuEntry extends SubsetsMenuEntry {
+    public final String text;
+    private int textWidth = -69;
+    private int x, y, width;
+    private boolean selected, containsMouse, rendering;
+    private List<SubsetsMenuEntry> entries;
+    private SubsetsMenu subsetsMenu;
+    private Pair<Integer, Integer> filteredRatio = null;
+    private long lastListHash = -1;
+    private boolean clickedBefore = false;
+    
+    public SubMenuEntry(String text) {
+        this(text, Collections.emptyList());
+    }
+    
+    public SubMenuEntry(String text, Supplier<List<SubsetsMenuEntry>> entries) {
+        this(text, entries.get());
+    }
+    
+    public SubMenuEntry(String text, List<SubsetsMenuEntry> entries) {
+        this.text = text;
+        this.entries = entries;
+    }
+    
+    private int getTextWidth() {
+        if (textWidth == -69) {
+            this.textWidth = Math.max(0, font.getStringWidth(text));
+        }
+        return this.textWidth;
+    }
+    
+    public SubsetsMenu getSubsetsMenu() {
+        if (subsetsMenu == null) {
+            this.subsetsMenu = new SubsetsMenu(new Point(getParent().getBounds().getMaxX() - 1, y - 1), entries);
+        }
+        return subsetsMenu;
+    }
+    
+    @Override
+    public int getEntryWidth() {
+        return 12 + getTextWidth() + 4;
+    }
+    
+    @Override
+    public int getEntryHeight() {
+        return 12;
+    }
+    
+    @Override
+    public void updateInformation(int xPos, int yPos, boolean selected, boolean containsMouse, boolean rendering, int width) {
+        this.x = xPos;
+        this.y = yPos;
+        this.selected = selected;
+        this.containsMouse = containsMouse;
+        this.rendering = rendering;
+        this.width = width;
+    }
+    
+    @Override
+    public void render(int mouseX, int mouseY, float delta) {
+        double filteredRatio = getFilteredRatio();
+        if (filteredRatio > 0) {
+            filteredRatio = filteredRatio * 0.85 + 0.15;
+            fill(x, y, x + width, y + 12, (16711680 | MathHelper.ceil(filteredRatio * 255.0) << 24) + (selected ? 39321 : 0));
+        } else if (selected) {
+            fill(x, y, x + width, y + 12, -12237499);
+        }
+        if (selected) {
+            if (!entries.isEmpty()) {
+                SubsetsMenu menu = getSubsetsMenu();
+                menu.menuStartPoint.x = getParent().getBounds().getMaxX() - 1;
+                menu.menuStartPoint.y = y - 1;
+                List<Rectangle> areas = Lists.newArrayList(ScissorsHandler.INSTANCE.getScissorsAreas());
+                ScissorsHandler.INSTANCE.clearScissors();
+                menu.render(mouseX, mouseY, delta);
+                for (Rectangle area : areas) {
+                    ScissorsHandler.INSTANCE.scissor(area);
+                }
+            } else clickedBefore = false;
+            if (clickedBefore) {
+                if (rendering && mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + 12 && !entries.isEmpty()) {
+                    REIHelper.getInstance().queueTooltip(Tooltip.create("Click again to filter everything in this group."));
+                } else clickedBefore = false;
+            }
+        } else clickedBefore = false;
+        font.draw(text, x + 2, y + 2, selected ? 16777215 : 8947848);
+        if (!entries.isEmpty()) {
+            MinecraftClient.getInstance().getTextureManager().bindTexture(TabWidget.CHEST_GUI_TEXTURE);
+            drawTexture(x + width - 15, y - 2, 0, 28, 18, 18);
+        }
+    }
+    
+    @Override
+    public boolean mouseClicked(double mouseX, double mouseY, int button) {
+        if (rendering && mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + 12 && !entries.isEmpty()) {
+            if (clickedBefore) {
+                clickedBefore = false;
+                List<EntryStack> filteredStacks = ConfigObject.getInstance().getFilteredStacks();
+                SubsetsMenu subsetsMenu = ScreenHelper.getLastOverlay().getSubsetsMenu();
+                setFiltered(filteredStacks, subsetsMenu, this, !(getFilteredRatio() > 0));
+                ConfigManager.getInstance().saveConfig();
+                ((EntryRegistryImpl) EntryRegistry.getInstance()).refilter();
+                if (ScreenHelper.getSearchField() != null)
+                    ContainerScreenOverlay.getEntryListWidget().updateSearch(ScreenHelper.getSearchField().getText(), true);
+            } else {
+                clickedBefore = true;
+            }
+            minecraft.getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
+            return true;
+        }
+        return super.mouseClicked(mouseX, mouseY, button);
+    }
+    
+    private void setFiltered(List<EntryStack> filteredStacks, SubsetsMenu subsetsMenu, SubMenuEntry subMenuEntry, boolean filtered) {
+        for (SubsetsMenuEntry entry : subMenuEntry.entries) {
+            if (entry instanceof EntryStackMenuEntry) {
+                if (((EntryStackMenuEntry) entry).isFiltered() != filtered) {
+                    if (!filtered) {
+                        filteredStacks.removeIf(next -> next.equalsIgnoreAmount(((EntryStackMenuEntry) entry).stack));
+                    } else {
+                        filteredStacks.add(((EntryStackMenuEntry) entry).stack.copy());
+                    }
+                }
+                if (subsetsMenu != null)
+                    ((EntryStackMenuEntry) entry).recalculateFilter(subsetsMenu);
+            } else if (entry instanceof SubMenuEntry) {
+                setFiltered(filteredStacks, subsetsMenu, (SubMenuEntry) entry, filtered);
+            }
+        }
+    }
+    
+    public double getFilteredRatio() {
+        Pair<Integer, Integer> pair = getFilteredRatioPair();
+        return pair.getRight() == 0 ? 0 : pair.getLeft() / (double) pair.getRight();
+    }
+    
+    public Pair<Integer, Integer> getFilteredRatioPair() {
+        List<EntryStack> filteredStacks = ConfigObject.getInstance().getFilteredStacks();
+        if (lastListHash != filteredStacks.hashCode()) {
+            int size = 0;
+            int filtered = 0;
+            for (SubsetsMenuEntry entry : entries) {
+                if (entry instanceof EntryStackMenuEntry) {
+                    size++;
+                    if (((EntryStackMenuEntry) entry).isFiltered())
+                        filtered++;
+                } else if (entry instanceof SubMenuEntry) {
+                    Pair<Integer, Integer> pair = ((SubMenuEntry) entry).getFilteredRatioPair();
+                    filtered += pair.getLeft();
+                    size += pair.getRight();
+                }
+            }
+            filteredRatio = new Pair<>(filtered, size);
+            lastListHash = filteredStacks.hashCode();
+        }
+        return filteredRatio;
+    }
+    
+    @Override
+    public boolean containsMouse(double mouseX, double mouseY) {
+        if (super.containsMouse(mouseX, mouseY))
+            return true;
+        if (subsetsMenu != null && !subsetsMenu.children().isEmpty() && selected)
+            return subsetsMenu.containsMouse(mouseX, mouseY);
+        return false;
+    }
+    
+    @Override
+    public boolean mouseScrolled(double mouseX, double mouseY, double amount) {
+        return subsetsMenu != null && !subsetsMenu.children().isEmpty() && selected && subsetsMenu.mouseScrolled(mouseX, mouseY, amount);
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        if (subsetsMenu != null && !subsetsMenu.children().isEmpty() && selected) {
+            return Collections.singletonList(subsetsMenu);
+        }
+        return Collections.emptyList();
+    }
+}

+ 34 - 130
src/main/java/me/shedaniel/rei/gui/widget/EntryListWidget.java

@@ -24,7 +24,6 @@
 package me.shedaniel.rei.gui.widget;
 
 import com.google.common.collect.Lists;
-import com.mojang.blaze3d.systems.RenderSystem;
 import me.shedaniel.clothconfig2.ClothConfigInitializer;
 import me.shedaniel.clothconfig2.api.ScissorsHandler;
 import me.shedaniel.clothconfig2.gui.widget.DynamicNewSmoothScrollingEntryListWidget;
@@ -43,10 +42,8 @@ import me.shedaniel.rei.utils.CollectionUtils;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.screen.Screen;
 import net.minecraft.client.network.ClientPlayerEntity;
-import net.minecraft.client.render.BufferBuilder;
 import net.minecraft.client.render.Tessellator;
 import net.minecraft.client.render.VertexConsumerProvider;
-import net.minecraft.client.render.VertexFormats;
 import net.minecraft.client.resource.language.I18n;
 import net.minecraft.client.util.math.Matrix4f;
 import net.minecraft.client.util.math.MatrixStack;
@@ -81,10 +78,19 @@ public class EntryListWidget extends WidgetWithBounds {
     private static final int SIZE = 18;
     private static final boolean LAZY = true;
     private static int page;
-    protected double target;
-    protected double scroll;
-    protected long start;
-    protected long duration;
+    protected final ScrollingContainer scrolling = new ScrollingContainer() {
+        @Override
+        public Rectangle getBounds() {
+            return EntryListWidget.this.getBounds();
+        }
+        
+        @Override
+        public int getMaxScrollHeight() {
+            if (favorites.isEmpty())
+                return MathHelper.ceil((allStacks.size() + blockedCount) / (innerBounds.width / (float) entrySize())) * entrySize();
+            return MathHelper.ceil((allStacks.size() + blockedCount + getScrollNumberForFavorites()) / (innerBounds.width / (float) entrySize())) * entrySize() - 12;
+        }
+    };
     protected int blockedCount;
     private boolean debugTime;
     private Rectangle bounds, innerBounds;
@@ -95,7 +101,6 @@ public class EntryListWidget extends WidgetWithBounds {
     private List<Widget> widgets = Collections.emptyList();
     private List<SearchArgument.SearchArguments> lastSearchArguments = Collections.emptyList();
     private String lastSearchTerm = null;
-    private boolean draggingScrollBar = false;
     
     public static int entrySize() {
         return MathHelper.ceil(SIZE * ConfigObject.getInstance().getEntrySize());
@@ -142,46 +147,10 @@ public class EntryListWidget extends WidgetWithBounds {
         return (innerBounds.width / entrySize()) * getSlotsHeightNumberForFavorites();
     }
     
-    protected final int getMaxScrollPosition() {
-        if (favorites.isEmpty())
-            return MathHelper.ceil((allStacks.size() + blockedCount) / (innerBounds.width / (float) entrySize())) * entrySize();
-        return MathHelper.ceil((allStacks.size() + blockedCount + getScrollNumberForFavorites()) / (innerBounds.width / (float) entrySize())) * entrySize() - 12;
-    }
-    
-    protected final int getMaxScroll() {
-        return Math.max(0, this.getMaxScrollPosition() - innerBounds.height);
-    }
-    
-    protected final double clamp(double v) {
-        return this.clamp(v, 200.0D);
-    }
-    
-    protected final double clamp(double v, double clampExtension) {
-        return MathHelper.clamp(v, -clampExtension, (double) this.getMaxScroll() + clampExtension);
-    }
-    
-    protected final void offset(double value, boolean animated) {
-        scrollTo(target + value, animated);
-    }
-    
-    protected final void scrollTo(double value, boolean animated) {
-        scrollTo(value, animated, ClothConfigInitializer.getScrollDuration());
-    }
-    
-    protected final void scrollTo(double value, boolean animated, long duration) {
-        target = clamp(value);
-        
-        if (animated) {
-            start = System.currentTimeMillis();
-            this.duration = duration;
-        } else
-            scroll = target;
-    }
-    
     @Override
     public boolean mouseScrolled(double double_1, double double_2, double double_3) {
         if (ConfigObject.getInstance().isEntryListWidgetScrolled() && bounds.contains(double_1, double_2)) {
-            offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
+            scrolling.offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
             return true;
         }
         return super.mouseScrolled(double_1, double_2, double_3);
@@ -222,7 +191,7 @@ public class EntryListWidget extends WidgetWithBounds {
                 entry.clearStacks();
             ScissorsHandler.INSTANCE.scissor(bounds);
             int sizeForFavorites = getSlotsHeightNumberForFavorites();
-            int skip = Math.max(0, MathHelper.floor(scroll / (float) entrySize()) - sizeForFavorites);
+            int skip = Math.max(0, MathHelper.floor(scrolling.scrollAmount / (float) entrySize()) - sizeForFavorites);
             int nextIndex = skip * innerBounds.width / entrySize();
             int i = nextIndex;
             blockedCount = 0;
@@ -231,13 +200,13 @@ public class EntryListWidget extends WidgetWithBounds {
                 int size = 0;
                 long time = 0;
                 if (sizeForFavorites > 0) {
-                    drawString(font, I18n.translate("text.rei.favorites"), innerBounds.x + 2, (int) (innerBounds.y + 8 - scroll), -1);
+                    drawString(font, I18n.translate("text.rei.favorites"), innerBounds.x + 2, (int) (innerBounds.y + 8 - scrolling.scrollAmount), -1);
                     nextIndex += innerBounds.width / entrySize();
                     back1:
                     for (EntryStack stack : favorites) {
                         while (true) {
                             EntryListEntry entry = entries.get(nextIndex);
-                            entry.getBounds().y = (int) (entry.backupY - scroll);
+                            entry.getBounds().y = (int) (entry.backupY - scrolling.scrollAmount);
                             if (entry.getBounds().y > bounds.getMaxY())
                                 break back1;
                             if (notSteppingOnExclusionZones(entry.getBounds().x, entry.getBounds().y, innerBounds)) {
@@ -263,7 +232,7 @@ public class EntryListWidget extends WidgetWithBounds {
                     EntryStack stack = allStacks.get(i);
                     while (true) {
                         EntryListEntry entry = entries.get(nextIndex);
-                        entry.getBounds().y = (int) (entry.backupY - scroll + offset);
+                        entry.getBounds().y = (int) (entry.backupY - scrolling.scrollAmount + offset);
                         if (entry.getBounds().y > bounds.getMaxY())
                             break back;
                         if (notSteppingOnExclusionZones(entry.getBounds().x, entry.getBounds().y, innerBounds)) {
@@ -297,13 +266,13 @@ public class EntryListWidget extends WidgetWithBounds {
                 setZ(z);
             } else {
                 if (sizeForFavorites > 0) {
-                    drawString(font, I18n.translate("text.rei.favorites"), innerBounds.x + 2, (int) (innerBounds.y + 8 - scroll), -1);
+                    drawString(font, I18n.translate("text.rei.favorites"), innerBounds.x + 2, (int) (innerBounds.y + 8 - scrolling.scrollAmount), -1);
                     nextIndex += innerBounds.width / entrySize();
                     back1:
                     for (EntryStack stack : favorites) {
                         while (true) {
                             EntryListEntry entry = entries.get(nextIndex);
-                            entry.getBounds().y = (int) (entry.backupY - scroll);
+                            entry.getBounds().y = (int) (entry.backupY - scrolling.scrollAmount);
                             if (entry.getBounds().y > bounds.getMaxY())
                                 break back1;
                             if (notSteppingOnExclusionZones(entry.getBounds().x, entry.getBounds().y, innerBounds)) {
@@ -326,7 +295,7 @@ public class EntryListWidget extends WidgetWithBounds {
                     EntryStack stack = allStacks.get(i);
                     while (true) {
                         EntryListEntry entry = entries.get(nextIndex);
-                        entry.getBounds().y = (int) (entry.backupY - scroll + offset);
+                        entry.getBounds().y = (int) (entry.backupY - scrolling.scrollAmount + offset);
                         if (entry.getBounds().y > bounds.getMaxY())
                             break back;
                         if (notSteppingOnExclusionZones(entry.getBounds().x, entry.getBounds().y, innerBounds)) {
@@ -344,7 +313,7 @@ public class EntryListWidget extends WidgetWithBounds {
             }
             updatePosition(delta);
             ScissorsHandler.INSTANCE.removeLastScissor();
-            renderScrollbar();
+            scrolling.renderScrollBar();
         } else {
             if (debugTime) {
                 int size = 0;
@@ -473,78 +442,21 @@ public class EntryListWidget extends WidgetWithBounds {
     }
     
     @Override
-    public boolean mouseDragged(double mouseX, double mouseY, int int_1, double double_3, double double_4) {
-        if (int_1 == 0 && draggingScrollBar) {
-            float height = getMaxScrollPosition();
-            int actualHeight = innerBounds.height;
-            if (height > actualHeight && mouseY >= innerBounds.y && mouseY <= innerBounds.getMaxY()) {
-                double double_5 = Math.max(1, this.getMaxScroll());
-                int int_2 = innerBounds.height;
-                int int_3 = MathHelper.clamp((int) ((float) (int_2 * int_2) / (float) getMaxScrollPosition()), 32, int_2 - 8);
-                double double_6 = Math.max(1.0D, double_5 / (double) (int_2 - int_3));
-                float to = MathHelper.clamp((float) (scroll + double_4 * double_6), 0, height - innerBounds.height);
-                if (ConfigObject.getInstance().doesSnapToRows()) {
-                    double nearestRow = Math.round(to / (double) entrySize()) * (double) entrySize();
-                    scrollTo(nearestRow, false);
-                } else
-                    scrollTo(to, false);
-            }
-        }
-        return super.mouseDragged(mouseX, mouseY, int_1, double_3, double_4);
-    }
-    
-    private void renderScrollbar() {
-        int maxScroll = getMaxScroll();
-        if (maxScroll > 0) {
-            int height = innerBounds.height * innerBounds.height / getMaxScrollPosition();
-            height = MathHelper.clamp(height, 32, innerBounds.height - 8);
-            height -= Math.min((scroll < 0 ? (int) -scroll : scroll > maxScroll ? (int) scroll - maxScroll : 0), height * .95);
-            height = Math.max(10, height);
-            int minY = Math.min(Math.max((int) scroll * (innerBounds.height - height) / maxScroll + innerBounds.y, innerBounds.y), innerBounds.getMaxY() - height);
-            
-            int scrollbarPositionMinX = getScrollbarMinX();
-            int scrollbarPositionMaxX = scrollbarPositionMinX + 6;
-            boolean hovered = (new Rectangle(scrollbarPositionMinX, minY, scrollbarPositionMaxX - scrollbarPositionMinX, height)).contains(PointHelper.ofMouse());
-            float bottomC = (hovered ? .67f : .5f) * (REIHelper.getInstance().isDarkThemeEnabled() ? 0.8f : 1f);
-            float topC = (hovered ? .87f : .67f) * (REIHelper.getInstance().isDarkThemeEnabled() ? 0.8f : 1f);
-            
-            RenderSystem.disableTexture();
-            RenderSystem.enableBlend();
-            RenderSystem.disableAlphaTest();
-            RenderSystem.blendFuncSeparate(770, 771, 1, 0);
-            RenderSystem.shadeModel(7425);
-            Tessellator tessellator = Tessellator.getInstance();
-            BufferBuilder buffer = tessellator.getBuffer();
-            buffer.begin(7, VertexFormats.POSITION_COLOR);
-            buffer.vertex(scrollbarPositionMinX, minY + height, 0.0D).color(bottomC, bottomC, bottomC, 1).next();
-            buffer.vertex(scrollbarPositionMaxX, minY + height, 0.0D).color(bottomC, bottomC, bottomC, 1).next();
-            buffer.vertex(scrollbarPositionMaxX, minY, 0.0D).color(bottomC, bottomC, bottomC, 1).next();
-            buffer.vertex(scrollbarPositionMinX, minY, 0.0D).color(bottomC, bottomC, bottomC, 1).next();
-            tessellator.draw();
-            buffer.begin(7, VertexFormats.POSITION_COLOR);
-            buffer.vertex(scrollbarPositionMinX, (minY + height - 1), 0.0D).color(topC, topC, topC, 1).next();
-            buffer.vertex((scrollbarPositionMaxX - 1), (minY + height - 1), 0.0D).color(topC, topC, topC, 1).next();
-            buffer.vertex((scrollbarPositionMaxX - 1), minY, 0.0D).color(topC, topC, topC, 1).next();
-            buffer.vertex(scrollbarPositionMinX, minY, 0.0D).color(topC, topC, topC, 1).next();
-            tessellator.draw();
-            RenderSystem.shadeModel(7424);
-            RenderSystem.disableBlend();
-            RenderSystem.enableAlphaTest();
-            RenderSystem.enableTexture();
-        }
+    public boolean mouseDragged(double mouseX, double mouseY, int button, double dx, double dy) {
+        if (scrolling.mouseDragged(mouseX, mouseY, button, dx, dy, true))
+            return true;
+        return super.mouseDragged(mouseX, mouseY, button, dx, dy);
     }
     
     private void updatePosition(float delta) {
-        if (ConfigObject.getInstance().doesSnapToRows() && target >= 0 && target <= getMaxScroll()) {
-            double nearestRow = Math.round(target / (double) entrySize()) * (double) entrySize();
-            if (!DynamicNewSmoothScrollingEntryListWidget.Precision.almostEquals(target, nearestRow, DynamicNewSmoothScrollingEntryListWidget.Precision.FLOAT_EPSILON))
-                target += (nearestRow - target) * Math.min(delta / 2.0, 1.0);
+        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
-                target = nearestRow;
+                scrolling.scrollTarget = nearestRow;
         }
-        double[] targetD = new double[]{this.target};
-        this.scroll = ClothConfigInitializer.handleScrollingPosition(targetD, this.scroll, this.getMaxScroll(), delta, this.start, this.duration);
-        this.target = targetD[0];
+        scrolling.updatePosition(delta);
     }
     
     @Override
@@ -748,16 +660,8 @@ public class EntryListWidget extends WidgetWithBounds {
     @Override
     public boolean mouseClicked(double double_1, double double_2, int int_1) {
         if (ConfigObject.getInstance().isEntryListWidgetScrolled()) {
-            double height = getMaxScroll();
-            int actualHeight = bounds.height;
-            if (height > actualHeight && double_2 >= bounds.y && double_2 <= bounds.getMaxY()) {
-                double scrollbarPositionMinX = getScrollbarMinX();
-                if (double_1 >= scrollbarPositionMinX - 1 & double_1 <= scrollbarPositionMinX + 8) {
-                    this.draggingScrollBar = true;
-                    return true;
-                }
-            }
-            this.draggingScrollBar = false;
+            if (scrolling.updateDraggingState(double_1, double_2, int_1))
+                return true;
         }
         
         if (containsMouse(double_1, double_2)) {

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

@@ -373,9 +373,13 @@ public class EntryWidget extends Slot {
                     else if (!CollectionUtils.anyMatchEqualsEntryIgnoreAmount(ConfigObject.getInstance().getFavorites(), entry))
                         ConfigObject.getInstance().getFavorites().add(entry);
                     ConfigManager.getInstance().saveConfig();
-                    FavoritesListWidget favoritesListWidget = ContainerScreenOverlay.getFavoritesListWidget();
-                    if (favoritesListWidget != null)
-                        favoritesListWidget.updateSearch(ContainerScreenOverlay.getEntryListWidget(), ScreenHelper.getSearchField().getText());
+                    if (ConfigObject.getInstance().doDisplayFavoritesOnTheLeft()) {
+                        FavoritesListWidget favoritesListWidget = ContainerScreenOverlay.getFavoritesListWidget();
+                        if (favoritesListWidget != null)
+                            favoritesListWidget.updateSearch(ContainerScreenOverlay.getEntryListWidget(), ScreenHelper.getSearchField().getText());
+                    } else {
+                        ContainerScreenOverlay.getEntryListWidget().updateSearch(ScreenHelper.getSearchField().getText());
+                    }
                     minecraft.getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
                     return true;
                 }

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

@@ -24,7 +24,6 @@
 package me.shedaniel.rei.gui.widget;
 
 import com.google.common.collect.Lists;
-import com.mojang.blaze3d.systems.RenderSystem;
 import me.shedaniel.clothconfig2.ClothConfigInitializer;
 import me.shedaniel.clothconfig2.api.ScissorsHandler;
 import me.shedaniel.clothconfig2.gui.widget.DynamicNewSmoothScrollingEntryListWidget;
@@ -40,9 +39,6 @@ import me.shedaniel.rei.utils.CollectionUtils;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.screen.Screen;
 import net.minecraft.client.network.ClientPlayerEntity;
-import net.minecraft.client.render.BufferBuilder;
-import net.minecraft.client.render.Tessellator;
-import net.minecraft.client.render.VertexFormats;
 import net.minecraft.client.resource.language.I18n;
 import net.minecraft.util.math.MathHelper;
 import org.jetbrains.annotations.ApiStatus;
@@ -56,10 +52,24 @@ import static me.shedaniel.rei.gui.widget.EntryListWidget.*;
 
 @ApiStatus.Internal
 public class FavoritesListWidget extends WidgetWithBounds {
-    protected double target;
-    protected double scroll;
-    protected long start;
-    protected long duration;
+    protected final ScrollingContainer scrolling = new ScrollingContainer() {
+        @Override
+        public Rectangle getBounds() {
+            return bounds;
+        }
+        
+        @Override
+        public int getMaxScrollHeight() {
+            return MathHelper.ceil((favorites.size() + blockedCount) / (innerBounds.width / (float) entrySize())) * entrySize();
+        }
+    
+        @Override
+        public int getScrollBarX() {
+            if (!ConfigObject.getInstance().isLeftHandSidePanel())
+                return bounds.x + 1;
+            return bounds.getMaxX() - 7;
+        }
+    };
     protected int blockedCount;
     List<EntryStack> favorites = null;
     private Rectangle bounds, innerBounds;
@@ -73,44 +83,10 @@ public class FavoritesListWidget extends WidgetWithBounds {
         return new Rectangle((int) (bounds.getCenterX() - width * (entrySize() / 2f) - 3), bounds.y, width * entrySize(), bounds.height);
     }
     
-    protected final int getMaxScrollPosition() {
-        return MathHelper.ceil((favorites.size() + blockedCount) / (innerBounds.width / (float) entrySize())) * entrySize();
-    }
-    
-    protected final int getMaxScroll() {
-        return Math.max(0, this.getMaxScrollPosition() - innerBounds.height);
-    }
-    
-    protected final double clamp(double v) {
-        return this.clamp(v, 200.0D);
-    }
-    
-    protected final double clamp(double v, double clampExtension) {
-        return MathHelper.clamp(v, -clampExtension, (double) this.getMaxScroll() + clampExtension);
-    }
-    
-    protected final void offset(double value, boolean animated) {
-        scrollTo(target + value, animated);
-    }
-    
-    protected final void scrollTo(double value, boolean animated) {
-        scrollTo(value, animated, ClothConfigInitializer.getScrollDuration());
-    }
-    
-    protected final void scrollTo(double value, boolean animated, long duration) {
-        target = clamp(value);
-        
-        if (animated) {
-            start = System.currentTimeMillis();
-            this.duration = duration;
-        } else
-            scroll = target;
-    }
-    
     @Override
     public boolean mouseScrolled(double double_1, double double_2, double double_3) {
         if (bounds.contains(double_1, double_2)) {
-            offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
+            scrolling.offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
             return true;
         }
         return super.mouseScrolled(double_1, double_2, double_3);
@@ -129,7 +105,7 @@ public class FavoritesListWidget extends WidgetWithBounds {
         for (EntryListEntry entry : entries)
             entry.clearStacks();
         ScissorsHandler.INSTANCE.scissor(bounds);
-        int skip = Math.max(0, MathHelper.floor(scroll / (float) entrySize()));
+        int skip = Math.max(0, MathHelper.floor(scrolling.scrollAmount / (float) entrySize()));
         int nextIndex = skip * innerBounds.width / entrySize();
         int i = nextIndex;
         blockedCount = 0;
@@ -138,7 +114,7 @@ public class FavoritesListWidget extends WidgetWithBounds {
             EntryStack stack = favorites.get(i);
             while (true) {
                 EntryListEntry entry = entries.get(nextIndex);
-                entry.getBounds().y = (int) (entry.backupY - scroll);
+                entry.getBounds().y = (int) (entry.backupY - scrolling.scrollAmount);
                 if (entry.getBounds().y > bounds.getMaxY())
                     break back;
                 if (notSteppingOnExclusionZones(entry.getBounds().x, entry.getBounds().y, innerBounds)) {
@@ -153,91 +129,28 @@ public class FavoritesListWidget extends WidgetWithBounds {
             }
         }
         updatePosition(delta);
+        scrolling.renderScrollBar();
         ScissorsHandler.INSTANCE.removeLastScissor();
-        renderScrollbar();
         if (containsMouse(mouseX, mouseY) && ClientHelper.getInstance().isCheating() && !minecraft.player.inventory.getCursorStack().isEmpty() && RoughlyEnoughItemsCore.hasPermissionToUsePackets())
             Tooltip.create(I18n.translate("text.rei.delete_items")).queue();
     }
     
-    private int getScrollbarMinX() {
-        if (!ConfigObject.getInstance().isLeftHandSidePanel())
-            return bounds.x + 1;
-        return bounds.getMaxX() - 7;
-    }
-    
     @Override
     public boolean mouseDragged(double mouseX, double mouseY, int int_1, double double_3, double double_4) {
-        if (int_1 == 0 && draggingScrollBar) {
-            float height = getMaxScrollPosition();
-            int actualHeight = innerBounds.height;
-            if (height > actualHeight && mouseY >= innerBounds.y && mouseY <= innerBounds.getMaxY()) {
-                double double_5 = Math.max(1, this.getMaxScroll());
-                int int_2 = innerBounds.height;
-                int int_3 = MathHelper.clamp((int) ((float) (int_2 * int_2) / (float) getMaxScrollPosition()), 32, int_2 - 8);
-                double double_6 = Math.max(1.0D, double_5 / (double) (int_2 - int_3));
-                float to = MathHelper.clamp((float) (scroll + double_4 * double_6), 0, height - innerBounds.height);
-                if (ConfigObject.getInstance().doesSnapToRows()) {
-                    double nearestRow = Math.round(to / (double) entrySize()) * (double) entrySize();
-                    scrollTo(nearestRow, false);
-                } else
-                    scrollTo(to, false);
-            }
-        }
+        if (scrolling.mouseDragged(mouseX, mouseY, int_1, double_3, double_4, true))
+            return true;
         return super.mouseDragged(mouseX, mouseY, int_1, double_3, double_4);
     }
     
-    private void renderScrollbar() {
-        int maxScroll = getMaxScroll();
-        if (maxScroll > 0) {
-            int height = innerBounds.height * innerBounds.height / getMaxScrollPosition();
-            height = MathHelper.clamp(height, 32, innerBounds.height - 8);
-            height -= Math.min((scroll < 0 ? (int) -scroll : scroll > maxScroll ? (int) scroll - maxScroll : 0), height * .95);
-            height = Math.max(10, height);
-            int minY = Math.min(Math.max((int) scroll * (innerBounds.height - height) / maxScroll + innerBounds.y, innerBounds.y), innerBounds.getMaxY() - height);
-            
-            int scrollbarPositionMinX = getScrollbarMinX();
-            int scrollbarPositionMaxX = scrollbarPositionMinX + 6;
-            boolean hovered = (new Rectangle(scrollbarPositionMinX, minY, scrollbarPositionMaxX - scrollbarPositionMinX, height)).contains(PointHelper.ofMouse());
-            float bottomC = (hovered ? .67f : .5f) * (REIHelper.getInstance().isDarkThemeEnabled() ? 0.8f : 1f);
-            float topC = (hovered ? .87f : .67f) * (REIHelper.getInstance().isDarkThemeEnabled() ? 0.8f : 1f);
-            
-            RenderSystem.disableTexture();
-            RenderSystem.enableBlend();
-            RenderSystem.disableAlphaTest();
-            RenderSystem.blendFuncSeparate(770, 771, 1, 0);
-            RenderSystem.shadeModel(7425);
-            Tessellator tessellator = Tessellator.getInstance();
-            BufferBuilder buffer = tessellator.getBuffer();
-            buffer.begin(7, VertexFormats.POSITION_COLOR);
-            buffer.vertex(scrollbarPositionMinX, minY + height, 0.0D).color(bottomC, bottomC, bottomC, 1).next();
-            buffer.vertex(scrollbarPositionMaxX, minY + height, 0.0D).color(bottomC, bottomC, bottomC, 1).next();
-            buffer.vertex(scrollbarPositionMaxX, minY, 0.0D).color(bottomC, bottomC, bottomC, 1).next();
-            buffer.vertex(scrollbarPositionMinX, minY, 0.0D).color(bottomC, bottomC, bottomC, 1).next();
-            tessellator.draw();
-            buffer.begin(7, VertexFormats.POSITION_COLOR);
-            buffer.vertex(scrollbarPositionMinX, (minY + height - 1), 0.0D).color(topC, topC, topC, 1).next();
-            buffer.vertex((scrollbarPositionMaxX - 1), (minY + height - 1), 0.0D).color(topC, topC, topC, 1).next();
-            buffer.vertex((scrollbarPositionMaxX - 1), minY, 0.0D).color(topC, topC, topC, 1).next();
-            buffer.vertex(scrollbarPositionMinX, minY, 0.0D).color(topC, topC, topC, 1).next();
-            tessellator.draw();
-            RenderSystem.shadeModel(7424);
-            RenderSystem.disableBlend();
-            RenderSystem.enableAlphaTest();
-            RenderSystem.enableTexture();
-        }
-    }
-    
     private void updatePosition(float delta) {
-        if (ConfigObject.getInstance().doesSnapToRows() && target >= 0 && target <= getMaxScroll()) {
-            double nearestRow = Math.round(target / (double) entrySize()) * (double) entrySize();
-            if (!DynamicNewSmoothScrollingEntryListWidget.Precision.almostEquals(target, nearestRow, DynamicNewSmoothScrollingEntryListWidget.Precision.FLOAT_EPSILON))
-                target += (nearestRow - target) * Math.min(delta / 2.0, 1.0);
+        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
-                target = nearestRow;
+                scrolling.scrollTarget = nearestRow;
         }
-        double[] targetD = new double[]{this.target};
-        this.scroll = ClothConfigInitializer.handleScrollingPosition(targetD, this.scroll, this.getMaxScroll(), delta, this.start, this.duration);
-        this.target = targetD[0];
+        scrolling.updatePosition(delta);
     }
     
     @Override
@@ -325,16 +238,8 @@ public class FavoritesListWidget extends WidgetWithBounds {
     
     @Override
     public boolean mouseClicked(double double_1, double double_2, int int_1) {
-        double height = getMaxScroll();
-        int actualHeight = bounds.height;
-        if (height > actualHeight && double_2 >= bounds.y && double_2 <= bounds.getMaxY()) {
-            double scrollbarPositionMinX = getScrollbarMinX();
-            if (double_1 >= scrollbarPositionMinX - 1 & double_1 <= scrollbarPositionMinX + 8) {
-                this.draggingScrollBar = true;
-                return true;
-            }
-        }
-        this.draggingScrollBar = false;
+        if (scrolling.updateDraggingState(double_1, double_2, int_1))
+            return true;
         
         if (containsMouse(double_1, double_2)) {
             ClientPlayerEntity player = minecraft.player;

+ 0 - 1
src/main/java/me/shedaniel/rei/gui/widget/LateRenderable.java

@@ -27,5 +27,4 @@ import org.jetbrains.annotations.ApiStatus;
 
 @ApiStatus.Internal
 public interface LateRenderable {
-    void lateRender(int mouseX, int mouseY, float delta);
 }

+ 180 - 0
src/main/java/me/shedaniel/rei/gui/widget/ScrollingContainer.java

@@ -0,0 +1,180 @@
+package me.shedaniel.rei.gui.widget;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import me.shedaniel.clothconfig2.ClothConfigInitializer;
+import me.shedaniel.math.Rectangle;
+import me.shedaniel.math.impl.PointHelper;
+import me.shedaniel.rei.api.ConfigObject;
+import me.shedaniel.rei.api.REIHelper;
+import net.minecraft.client.render.BufferBuilder;
+import net.minecraft.client.render.Tessellator;
+import net.minecraft.client.render.VertexFormats;
+import net.minecraft.util.math.MathHelper;
+import org.jetbrains.annotations.ApiStatus;
+
+import static me.shedaniel.rei.gui.widget.EntryListWidget.entrySize;
+
+@ApiStatus.Internal
+public abstract class ScrollingContainer {
+    public double scrollAmount;
+    public double scrollTarget;
+    public long start;
+    public long duration;
+    public boolean draggingScrollBar = false;
+    
+    public abstract Rectangle getBounds();
+    
+    public Rectangle getScissorBounds() {
+        Rectangle bounds = getBounds();
+        if (hasScrollBar()) {
+            return new Rectangle(bounds.x, bounds.y, bounds.width - 6, bounds.height);
+        }
+        return bounds;
+    }
+    
+    public int getScrollBarX() {
+        return hasScrollBar() ? getBounds().getMaxX() - 6 : getBounds().getMaxX();
+    }
+    
+    public boolean hasScrollBar() {
+        return getMaxScrollHeight() > getBounds().height;
+    }
+    
+    public abstract int getMaxScrollHeight();
+    
+    public final int getMaxScroll() {
+        return Math.max(0, getMaxScrollHeight() - getBounds().height);
+    }
+    
+    public final double clamp(double v) {
+        return this.clamp(v, 200.0D);
+    }
+    
+    public final double clamp(double v, double clampExtension) {
+        return MathHelper.clamp(v, -clampExtension, (double) this.getMaxScroll() + clampExtension);
+    }
+    
+    public final void offset(double value, boolean animated) {
+        scrollTo(scrollTarget + value, animated);
+    }
+    
+    public final void scrollTo(double value, boolean animated) {
+        scrollTo(value, animated, ClothConfigInitializer.getScrollDuration());
+    }
+    
+    public final void scrollTo(double value, boolean animated, long duration) {
+        scrollTarget = clamp(value);
+        
+        if (animated) {
+            start = System.currentTimeMillis();
+            this.duration = duration;
+        } else
+            scrollAmount = scrollTarget;
+    }
+    
+    public void updatePosition(float delta) {
+        double[] target = new double[]{this.scrollTarget};
+        this.scrollAmount = ClothConfigInitializer.handleScrollingPosition(target, this.scrollAmount, this.getMaxScroll(), delta, this.start, this.duration);
+        this.scrollTarget = target[0];
+    }
+    
+    public void renderScrollBar() {
+        renderScrollBar(0, 1);
+    }
+    
+    public void renderScrollBar(int background, float alpha) {
+        if (hasScrollBar()) {
+            Rectangle bounds = getBounds();
+            int maxScroll = getMaxScroll();
+            int height = bounds.height * bounds.height / getMaxScrollHeight();
+            height = MathHelper.clamp(height, 32, bounds.height);
+            height -= Math.min((scrollAmount < 0 ? (int) -scrollAmount : scrollAmount > maxScroll ? (int) scrollAmount - maxScroll : 0), height * .95);
+            height = Math.max(10, height);
+            int minY = Math.min(Math.max((int) scrollAmount * (bounds.height - height) / maxScroll + bounds.y, bounds.y), bounds.getMaxY() - height);
+            
+            int scrollbarPositionMinX = getScrollBarX();
+            int scrollbarPositionMaxX = scrollbarPositionMinX + 6;
+            boolean hovered = (new Rectangle(scrollbarPositionMinX, minY, scrollbarPositionMaxX - scrollbarPositionMinX, height)).contains(PointHelper.ofMouse());
+            float bottomC = (hovered ? .67f : .5f) * (REIHelper.getInstance().isDarkThemeEnabled() ? 0.8f : 1f);
+            float topC = (hovered ? .87f : .67f) * (REIHelper.getInstance().isDarkThemeEnabled() ? 0.8f : 1f);
+            
+            RenderSystem.disableTexture();
+            RenderSystem.enableBlend();
+            RenderSystem.disableAlphaTest();
+            RenderSystem.blendFuncSeparate(770, 771, 1, 0);
+            RenderSystem.shadeModel(7425);
+            Tessellator tessellator = Tessellator.getInstance();
+            BufferBuilder buffer = tessellator.getBuffer();
+            {
+                float a = (background >> 24 & 255) / 255.0F;
+                float r = (background >> 16 & 255) / 255.0F;
+                float g = (background >> 8 & 255) / 255.0F;
+                float b = (background & 255) / 255.0F;
+                buffer.begin(7, VertexFormats.POSITION_COLOR);
+                buffer.vertex(scrollbarPositionMinX, bounds.getMaxY(), 0.0D).color(r, g, b, a).next();
+                buffer.vertex(scrollbarPositionMaxX, bounds.getMaxY(), 0.0D).color(r, g, b, a).next();
+                buffer.vertex(scrollbarPositionMaxX, bounds.y, 0.0D).color(r, g, b, a).next();
+                buffer.vertex(scrollbarPositionMinX, bounds.y, 0.0D).color(r, g, b, a).next();
+            }
+            tessellator.draw();
+            buffer.begin(7, VertexFormats.POSITION_COLOR);
+            buffer.vertex(scrollbarPositionMinX, minY + height, 0.0D).color(bottomC, bottomC, bottomC, alpha).next();
+            buffer.vertex(scrollbarPositionMaxX, minY + height, 0.0D).color(bottomC, bottomC, bottomC, alpha).next();
+            buffer.vertex(scrollbarPositionMaxX, minY, 0.0D).color(bottomC, bottomC, bottomC, alpha).next();
+            buffer.vertex(scrollbarPositionMinX, minY, 0.0D).color(bottomC, bottomC, bottomC, alpha).next();
+            tessellator.draw();
+            buffer.begin(7, VertexFormats.POSITION_COLOR);
+            buffer.vertex(scrollbarPositionMinX, (minY + height - 1), 0.0D).color(topC, topC, topC, alpha).next();
+            buffer.vertex((scrollbarPositionMaxX - 1), (minY + height - 1), 0.0D).color(topC, topC, topC, alpha).next();
+            buffer.vertex((scrollbarPositionMaxX - 1), minY, 0.0D).color(topC, topC, topC, alpha).next();
+            buffer.vertex(scrollbarPositionMinX, minY, 0.0D).color(topC, topC, topC, alpha).next();
+            tessellator.draw();
+            RenderSystem.shadeModel(7424);
+            RenderSystem.disableBlend();
+            RenderSystem.enableAlphaTest();
+            RenderSystem.enableTexture();
+        }
+    }
+    
+    public boolean mouseDragged(double mouseX, double mouseY, int button, double dx, double dy) {
+        return mouseDragged(mouseX, mouseY, button, dx, dy, false);
+    }
+    
+    public boolean mouseDragged(double mouseX, double mouseY, int button, double dx, double dy, boolean careSnapping) {
+        if (button == 0 && draggingScrollBar) {
+            float height = getMaxScrollHeight();
+            Rectangle bounds = getBounds();
+            int actualHeight = bounds.height;
+            if (mouseY >= bounds.y && mouseY <= bounds.getMaxY()) {
+                double maxScroll = Math.max(1, getMaxScroll());
+                double int_3 = MathHelper.clamp(((double) (actualHeight * actualHeight) / (double) height), 32, actualHeight - 8);
+                double double_6 = Math.max(1.0D, maxScroll / (actualHeight - int_3));
+                float to = MathHelper.clamp((float) (scrollAmount + dy * double_6), 0, getMaxScroll());
+                if (careSnapping && ConfigObject.getInstance().doesSnapToRows()) {
+                    double nearestRow = Math.round(to / (double) entrySize()) * (double) entrySize();
+                    scrollTo(nearestRow, false);
+                } else
+                    scrollTo(to, false);
+            }
+            return true;
+        }
+        return false;
+    }
+    
+    public boolean updateDraggingState(double mouseX, double mouseY, int button) {
+        if (!hasScrollBar())
+            return false;
+        double height = getMaxScroll();
+        Rectangle bounds = getBounds();
+        int actualHeight = bounds.height;
+        if (height > actualHeight && mouseY >= bounds.y && mouseY <= bounds.getMaxY()) {
+            double scrollbarPositionMinX = getScrollBarX();
+            if (mouseX >= scrollbarPositionMinX - 1 & mouseX <= scrollbarPositionMinX + 8) {
+                this.draggingScrollBar = true;
+                return true;
+            }
+        }
+        this.draggingScrollBar = false;
+        return false;
+    }
+}

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

@@ -77,10 +77,8 @@ public class ClientHelperImpl implements ClientHelper, ClientModInitializer {
         }
         return false;
     });
-    @ApiStatus.Internal public final Lazy<Boolean> ok = new Lazy<>(() -> {
+    @ApiStatus.Internal public final Lazy<Boolean> isAprilFools = new Lazy<>(() -> {
         try {
-            if (isYog.get())
-                return true;
             LocalDateTime now = LocalDateTime.now();
             return now.getMonthValue() == 4 && now.getDayOfMonth() == 1;
         } catch (Throwable ignored) {

+ 25 - 10
src/main/java/me/shedaniel/rei/impl/ConfigObjectImpl.java

@@ -32,10 +32,6 @@ import me.shedaniel.clothconfig2.api.ModifierKeyCode;
 import me.shedaniel.rei.api.ConfigObject;
 import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.gui.config.*;
-import me.shedaniel.rei.impl.ConfigObjectImpl.DontApplyFieldName;
-import me.shedaniel.rei.impl.ConfigObjectImpl.UseEnumSelectorInstead;
-import me.shedaniel.rei.impl.ConfigObjectImpl.UseFilteringScreen;
-import me.shedaniel.rei.impl.ConfigObjectImpl.UseSpecialRecipeTypeScreen;
 import net.minecraft.client.util.InputUtil;
 import org.jetbrains.annotations.ApiStatus;
 
@@ -50,12 +46,20 @@ 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("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("!general") @ConfigEntry.Gui.TransitiveObject @DontApplyFieldName
+    public General general = new General();
+    @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();
     
     @Override
     public boolean isOverlayVisible() {
@@ -326,6 +330,12 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
         return technical.debugSearchTimeRequired;
     }
     
+    @Override
+    @ApiStatus.Experimental
+    public boolean isSubsetsEnabled() {
+        return experimental.isSubsetsEnabled;
+    }
+    
     @Retention(RetentionPolicy.RUNTIME)
     @Target({ElementType.FIELD})
     @interface DontApplyFieldName {}
@@ -419,4 +429,9 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
     public static class Filtering {
         @UseFilteringScreen private List<EntryStack> filteredStacks = new ArrayList<>();
     }
+    
+    @ApiStatus.Experimental
+    public static class Experimental {
+        private boolean isSubsetsEnabled = false;
+    }
 }

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

@@ -173,7 +173,8 @@ public class EntryRegistryImpl implements EntryRegistry {
             getStacksList().addAll(stacks);
     }
     
-    private static class DefaultedLinkedList<E> extends DefaultedList<E> {
+    @ApiStatus.Internal
+    public static class DefaultedLinkedList<E> extends DefaultedList<E> {
         public DefaultedLinkedList(List<E> delegate, @Nullable E initialElement) {
             super(delegate, initialElement);
         }

+ 71 - 9
src/main/java/me/shedaniel/rei/impl/InternalWidgets.java

@@ -24,6 +24,7 @@
 package me.shedaniel.rei.impl;
 
 import com.google.common.collect.Lists;
+import com.mojang.blaze3d.systems.RenderSystem;
 import it.unimi.dsi.fastutil.ints.IntList;
 import me.shedaniel.math.Rectangle;
 import me.shedaniel.math.impl.PointHelper;
@@ -179,11 +180,15 @@ public final class InternalWidgets {
         };
     }
     
-    public static LateRenderable wrapLateRenderable(WidgetWithBounds widget) {
+    public static WidgetWithBounds wrapLateRenderable(WidgetWithBounds widget) {
         return new LateRenderableWidgetWithBounds(widget);
     }
     
-    public static LateRenderable wrapLateRenderable(Widget widget) {
+    public static WidgetWithBounds wrapTranslate(WidgetWithBounds widget, float x, float y, float z) {
+        return new WidgetWithBoundsWithTranslate(widget, x, y, z);
+    }
+    
+    public static Widget wrapLateRenderable(Widget widget) {
         return new LateRenderableWidget(widget);
     }
     
@@ -210,6 +215,15 @@ public final class InternalWidgets {
         public List<? extends Element> children() {
             return widgets;
         }
+    
+        @Override
+        public boolean mouseScrolled(double mouseX, double mouseY, double amount) {
+            for (Widget widget : this.widgets) {
+                if (widget.mouseScrolled(mouseX, mouseY, amount))
+                    return true;
+            }
+            return false;
+        }
     }
     
     private static class LateRenderableWidget extends Widget implements LateRenderable {
@@ -219,20 +233,21 @@ public final class InternalWidgets {
             this.widget = widget;
         }
         
-        
         @Override
-        public void lateRender(int mouseX, int mouseY, float delta) {
+        public void render(int mouseX, int mouseY, float delta) {
             this.widget.setZ(getZ());
             this.widget.render(mouseX, mouseY, delta);
         }
         
-        @Override
-        public void render(int mouseX, int mouseY, float delta) {}
-        
         @Override
         public List<? extends Element> children() {
             return Collections.singletonList(this.widget);
         }
+    
+        @Override
+        public boolean mouseScrolled(double mouseX, double mouseY, double amount) {
+            return this.widget.mouseScrolled(mouseX, mouseY, amount);
+        }
     }
     
     private static class LateRenderableWidgetWithBounds extends WidgetWithBounds implements LateRenderable {
@@ -242,24 +257,71 @@ public final class InternalWidgets {
             this.widget = widget;
         }
         
+        @Override
+        public @NotNull Rectangle getBounds() {
+            return this.widget.getBounds();
+        }
         
         @Override
-        public void lateRender(int mouseX, int mouseY, float delta) {
+        public void render(int mouseX, int mouseY, float delta) {
             this.widget.setZ(getZ());
             this.widget.render(mouseX, mouseY, delta);
         }
         
+        @Override
+        public boolean containsMouse(double mouseX, double mouseY) {
+            return this.widget.containsMouse(mouseX, mouseY);
+        }
+        
+        @Override
+        public List<? extends Element> children() {
+            return Collections.singletonList(this.widget);
+        }
+    
+        @Override
+        public boolean mouseScrolled(double mouseX, double mouseY, double amount) {
+            return this.widget.mouseScrolled(mouseX, mouseY, amount);
+        }
+    }
+    
+    private static class WidgetWithBoundsWithTranslate extends WidgetWithBounds implements LateRenderable {
+        private final WidgetWithBounds widget;
+        private final float x, y, z;
+        
+        public WidgetWithBoundsWithTranslate(WidgetWithBounds widget, float x, float y, float z) {
+            this.widget = widget;
+            this.x = x;
+            this.y = y;
+            this.z = z;
+        }
+        
         @Override
         public @NotNull Rectangle getBounds() {
             return this.widget.getBounds();
         }
         
         @Override
-        public void render(int mouseX, int mouseY, float delta) {}
+        public void render(int mouseX, int mouseY, float delta) {
+            RenderSystem.pushMatrix();
+            RenderSystem.translatef(x, y, z);
+            this.widget.setZ(getZ());
+            this.widget.render(mouseX, mouseY, delta);
+            RenderSystem.popMatrix();
+        }
+        
+        @Override
+        public boolean containsMouse(double mouseX, double mouseY) {
+            return this.widget.containsMouse(mouseX, mouseY);
+        }
         
         @Override
         public List<? extends Element> children() {
             return Collections.singletonList(this.widget);
         }
+    
+        @Override
+        public boolean mouseScrolled(double mouseX, double mouseY, double amount) {
+            return this.widget.mouseScrolled(mouseX, mouseY, amount);
+        }
     }
 }

+ 3 - 0
src/main/java/me/shedaniel/rei/impl/RecipeHelperImpl.java

@@ -30,6 +30,8 @@ import me.shedaniel.math.Rectangle;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.*;
 import me.shedaniel.rei.api.plugins.REIPluginV0;
+import me.shedaniel.rei.api.subsets.SubsetsRegistry;
+import me.shedaniel.rei.impl.subsets.SubsetsRegistryImpl;
 import me.shedaniel.rei.utils.CollectionUtils;
 import net.minecraft.client.gui.screen.ingame.HandledScreen;
 import net.minecraft.recipe.Recipe;
@@ -280,6 +282,7 @@ public class RecipeHelperImpl implements RecipeHelper {
         this.displayVisibilityHandlers.clear();
         this.liveRecipeGenerators.clear();
         this.autoTransferHandlers.clear();
+        ((SubsetsRegistryImpl) SubsetsRegistry.INSTANCE).reset();
         ((DisplayHelperImpl) DisplayHelper.getInstance()).resetData();
         ((DisplayHelperImpl) DisplayHelper.getInstance()).resetCache();
         BaseBoundsHandler baseBoundsHandler = new BaseBoundsHandlerImpl();

+ 7 - 0
src/main/java/me/shedaniel/rei/impl/SearchArgument.java

@@ -25,6 +25,7 @@ package me.shedaniel.rei.impl;
 
 import com.google.common.collect.Lists;
 import me.shedaniel.math.Point;
+import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.api.ClientHelper;
 import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.widgets.Tooltip;
@@ -187,6 +188,9 @@ public class SearchArgument {
             return tryGetItemStackName(stack.getItemStack());
         else if (stack.getType() == EntryStack.Type.FLUID)
             return tryGetFluidName(stack.getFluid());
+        Tooltip tooltip = stack.getTooltip(PointHelper.ofMouse());
+        if (tooltip != null)
+            return tooltip.getText().isEmpty() ? "" : tooltip.getText().get(0);
         return "";
     }
     
@@ -195,6 +199,9 @@ public class SearchArgument {
             return tryGetItemStackNameNoFormatting(stack.getItemStack());
         else if (stack.getType() == EntryStack.Type.FLUID)
             return tryGetFluidName(stack.getFluid());
+        Tooltip tooltip = stack.getTooltip(PointHelper.ofMouse());
+        if (tooltip != null)
+            return tooltip.getText().isEmpty() ? "" : tooltip.getText().get(0);
         return "";
     }
     

+ 83 - 0
src/main/java/me/shedaniel/rei/impl/subsets/SubsetsRegistryImpl.java

@@ -0,0 +1,83 @@
+package me.shedaniel.rei.impl.subsets;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.api.subsets.SubsetsRegistry;
+import me.shedaniel.rei.utils.CollectionUtils;
+import net.minecraft.util.Identifier;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+
+@ApiStatus.Experimental
+@ApiStatus.Internal
+public class SubsetsRegistryImpl implements SubsetsRegistry {
+    private final Map<String, Set<EntryStack>> entryPaths = Maps.newHashMap();
+    
+    public void reset() {
+        entryPaths.clear();
+    }
+    
+    @Override
+    public @NotNull List<String> getEntryPaths(@NotNull EntryStack stack) {
+        List<String> strings = null;
+        for (Map.Entry<String, Set<EntryStack>> entry : entryPaths.entrySet()) {
+            if (CollectionUtils.findFirstOrNullEqualsEntryIgnoreAmount(entry.getValue(), stack) != null) {
+                if (strings == null)
+                    strings = Lists.newArrayList();
+                strings.add(entry.getKey());
+            }
+        }
+        return strings == null ? Collections.emptyList() : strings;
+    }
+    
+    @Override
+    public void registerPathEntry(@NotNull String path, @NotNull EntryStack stack) {
+        getOrCreatePathEntries(path).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));
+    }
+    
+    @Override
+    public void registerPathEntries(@NotNull String path, @NotNull Collection<EntryStack> stacks) {
+        Set<EntryStack> entries = getOrCreatePathEntries(path);
+        for (EntryStack stack : stacks) {
+            entries.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));
+        }
+    }
+    
+    @Nullable
+    @Override
+    public Set<EntryStack> getPathEntries(@NotNull String path) {
+        if (!isPathValid(path))
+            throw new IllegalArgumentException("Illegal path: " + path);
+        return entryPaths.get(path);
+    }
+    
+    @Override
+    public @NotNull Set<String> getPaths() {
+        return entryPaths.keySet();
+    }
+    
+    @NotNull
+    @Override
+    public Set<EntryStack> getOrCreatePathEntries(@NotNull String path) {
+        Set<EntryStack> paths = getPathEntries(path);
+        if (paths == null) {
+            entryPaths.put(path, Sets.newLinkedHashSet());
+            paths = Objects.requireNonNull(getPathEntries(path));
+        }
+        return paths;
+    }
+    
+    private boolean isPathValid(String path) {
+        String[] pathSegments = path.split("/");
+        for (String pathSegment : pathSegments) {
+            if (!Identifier.isValid(pathSegment))
+                return false;
+        }
+        return true;
+    }
+}

+ 5 - 17
src/main/java/me/shedaniel/rei/plugin/DefaultPlugin.java

@@ -31,6 +31,7 @@ import me.shedaniel.math.Rectangle;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.*;
 import me.shedaniel.rei.api.plugins.REIPluginV0;
+import me.shedaniel.rei.api.subsets.SubsetsRegistry;
 import me.shedaniel.rei.api.widgets.Panel;
 import me.shedaniel.rei.api.widgets.Tooltip;
 import me.shedaniel.rei.gui.RecipeViewingScreen;
@@ -171,7 +172,7 @@ public class DefaultPlugin implements REIPluginV0 {
             
             @Override
             public boolean isEmpty() {
-                return !((ClientHelperImpl) ClientHelper.getInstance()).ok.get();
+                return !((ClientHelperImpl) ClientHelper.getInstance()).isAprilFools.get();
             }
             
             @Override
@@ -296,22 +297,6 @@ public class DefaultPlugin implements REIPluginV0 {
                 return Collections.emptyList();
             return Collections.singletonList(widget.getBounds().clone());
         });
-//        displayHelper.registerHandler(new OverlayDecider() {
-//            @Override
-//            public boolean isHandingScreen(Class<?> screen) {
-//                return InventoryScreen.class.isAssignableFrom(screen);
-//            }
-//
-//            @Override
-//            public ActionResult shouldScreenBeOverlayed(Class<?> screen) {
-//                return isHandingScreen(screen) ? ActionResult.FAIL : ActionResult.PASS;
-//            }
-//
-//            @Override
-//            public float getPriority() {
-//                return 10f;
-//            }
-//        });
         displayHelper.registerHandler(new DisplayHelper.DisplayBoundsHandler<HandledScreen<?>>() {
             @Override
             public Class<?> getBaseSupportedClass() {
@@ -404,6 +389,9 @@ public class DefaultPlugin implements REIPluginV0 {
         recipeHelper.registerScreenClickArea(new Rectangle(78, 32, 28, 23), FurnaceScreen.class, SMELTING);
         recipeHelper.registerScreenClickArea(new Rectangle(78, 32, 28, 23), SmokerScreen.class, SMOKING);
         recipeHelper.registerScreenClickArea(new Rectangle(78, 32, 28, 23), BlastFurnaceScreen.class, BLASTING);
+//        SubsetsRegistry subsetsRegistry = SubsetsRegistry.INSTANCE;
+//        subsetsRegistry.registerPathEntry("roughlyenoughitems:food", EntryStack.create(Items.MILK_BUCKET));
+//        subsetsRegistry.registerPathEntry("roughlyenoughitems:food/roughlyenoughitems:cookies", EntryStack.create(Items.COOKIE));
     }
     
     @Override

+ 32 - 108
src/main/java/me/shedaniel/rei/plugin/beacon/DefaultBeaconBaseCategory.java

@@ -24,18 +24,16 @@
 package me.shedaniel.rei.plugin.beacon;
 
 import com.google.common.collect.Lists;
-import com.mojang.blaze3d.systems.RenderSystem;
 import me.shedaniel.clothconfig2.ClothConfigInitializer;
 import me.shedaniel.clothconfig2.api.ScissorsHandler;
-import me.shedaniel.clothconfig2.gui.widget.DynamicEntryListWidget;
 import me.shedaniel.math.Point;
 import me.shedaniel.math.Rectangle;
-import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.RecipeCategory;
 import me.shedaniel.rei.api.widgets.Slot;
 import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.entries.RecipeEntry;
+import me.shedaniel.rei.gui.widget.ScrollingContainer;
 import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.gui.widget.WidgetWithBounds;
 import me.shedaniel.rei.plugin.DefaultPlugin;
@@ -43,9 +41,6 @@ import me.shedaniel.rei.utils.CollectionUtils;
 import net.minecraft.block.Blocks;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.Element;
-import net.minecraft.client.render.BufferBuilder;
-import net.minecraft.client.render.Tessellator;
-import net.minecraft.client.render.VertexFormats;
 import net.minecraft.client.resource.language.I18n;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.math.MathHelper;
@@ -109,10 +104,18 @@ public class DefaultBeaconBaseCategory implements RecipeCategory<DefaultBeaconBa
     private static class ScrollableSlotsWidget extends WidgetWithBounds {
         private Rectangle bounds;
         private List<Slot> widgets;
-        private double target;
-        private double scroll;
-        private long start;
-        private long duration;
+        private final ScrollingContainer scrolling = new ScrollingContainer() {
+            @Override
+            public Rectangle getBounds() {
+                Rectangle bounds = ScrollableSlotsWidget.this.getBounds();
+                return new Rectangle(bounds.x + 1, bounds.y + 1, bounds.width - 2, bounds.height - 2);
+            }
+            
+            @Override
+            public int getMaxScrollHeight() {
+                return MathHelper.ceil(widgets.size() / 8f) * 18;
+            }
+        };
         
         public ScrollableSlotsWidget(Rectangle bounds, List<Slot> widgets) {
             this.bounds = Objects.requireNonNull(bounds);
@@ -122,56 +125,36 @@ public class DefaultBeaconBaseCategory implements RecipeCategory<DefaultBeaconBa
         @Override
         public boolean mouseScrolled(double double_1, double double_2, double double_3) {
             if (containsMouse(double_1, double_2)) {
-                offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
+                scrolling.offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
                 return true;
             }
             return false;
         }
         
-        public void offset(double value, boolean animated) {
-            scrollTo(target + value, animated);
-        }
-        
-        public void scrollTo(double value, boolean animated) {
-            scrollTo(value, animated, ClothConfigInitializer.getScrollDuration());
-        }
-        
-        public void scrollTo(double value, boolean animated, long duration) {
-            target = clamp(value);
-            
-            if (animated) {
-                start = System.currentTimeMillis();
-                this.duration = duration;
-            } else
-                scroll = target;
-        }
-        
-        public final double clamp(double v) {
-            return clamp(v, DynamicEntryListWidget.SmoothScrollingSettings.CLAMP_EXTENSION);
-        }
-        
-        public final double clamp(double v, double clampExtension) {
-            return MathHelper.clamp(v, -clampExtension, getMaxScroll() + clampExtension);
-        }
-        
-        protected int getMaxScroll() {
-            return Math.max(0, this.getMaxScrollPosition() - this.getBounds().height + 1);
+        @NotNull
+        @Override
+        public Rectangle getBounds() {
+            return bounds;
         }
         
-        protected int getMaxScrollPosition() {
-            return MathHelper.ceil(widgets.size() / 8f) * 18;
+        @Override
+        public boolean mouseClicked(double mouseX, double mouseY, int button) {
+            if (scrolling.updateDraggingState(mouseX, mouseY, button))
+                return true;
+            return super.mouseClicked(mouseX, mouseY, button);
         }
         
-        @NotNull
         @Override
-        public Rectangle getBounds() {
-            return bounds;
+        public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
+            if (scrolling.mouseDragged(mouseX, mouseY, button, deltaX, deltaY))
+                return true;
+            return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY);
         }
         
         @Override
         public void render(int mouseX, int mouseY, float delta) {
-            updatePosition(delta);
-            Rectangle innerBounds = new Rectangle(bounds.x + 1, bounds.y + 1, bounds.width - 7, bounds.height - 2);
+            scrolling.updatePosition(delta);
+            Rectangle innerBounds = scrolling.getScissorBounds();
             ScissorsHandler.INSTANCE.scissor(innerBounds);
             for (int y = 0; y < MathHelper.ceil(widgets.size() / 8f); y++) {
                 for (int x = 0; x < 8; x++) {
@@ -179,75 +162,16 @@ public class DefaultBeaconBaseCategory implements RecipeCategory<DefaultBeaconBa
                     if (widgets.size() <= index)
                         break;
                     Slot widget = widgets.get(index);
-                    widget.getBounds().setLocation(bounds.x + 1 + x * 18, (int) (bounds.y + 1 + y * 18 - scroll));
+                    widget.getBounds().setLocation(bounds.x + 1 + x * 18, (int) (bounds.y + 1 + y * 18 - scrolling.scrollAmount));
                     widget.render(mouseX, mouseY, delta);
                 }
             }
             ScissorsHandler.INSTANCE.removeLastScissor();
-            ScissorsHandler.INSTANCE.scissor(bounds);
-            RenderSystem.enableBlend();
-            RenderSystem.blendFuncSeparate(770, 771, 0, 1);
-            RenderSystem.disableAlphaTest();
-            RenderSystem.shadeModel(7425);
-            RenderSystem.disableTexture();
-            renderScrollBar();
-            RenderSystem.enableTexture();
-            RenderSystem.shadeModel(7424);
-            RenderSystem.enableAlphaTest();
-            RenderSystem.disableBlend();
+            ScissorsHandler.INSTANCE.scissor(scrolling.getBounds());
+            scrolling.renderScrollBar(0xff000000, 1);
             ScissorsHandler.INSTANCE.removeLastScissor();
         }
         
-        @SuppressWarnings("deprecation")
-        private void renderScrollBar() {
-            int maxScroll = getMaxScroll();
-            int scrollbarPositionMinX = getBounds().getMaxX() - 7;
-            int scrollbarPositionMaxX = scrollbarPositionMinX + 6;
-            Tessellator tessellator = Tessellator.getInstance();
-            BufferBuilder buffer = tessellator.getBuffer();
-            if (maxScroll > 0) {
-                int height = (int) (((this.getBounds().height - 2f) * (this.getBounds().height - 2f)) / this.getMaxScrollPosition());
-                height = MathHelper.clamp(height, 32, this.getBounds().height - 2);
-                height -= Math.min((scroll < 0 ? (int) -scroll : scroll > maxScroll ? (int) scroll - maxScroll : 0), height * .95);
-                height = Math.max(10, height);
-                int minY = Math.min(Math.max((int) scroll * (this.getBounds().height - 2 - height) / maxScroll + getBounds().y + 1, getBounds().y + 1), getBounds().getMaxY() - 1 - height);
-                
-                boolean hovered = new Rectangle(scrollbarPositionMinX, minY, scrollbarPositionMaxX - scrollbarPositionMinX, height).contains(PointHelper.ofMouse());
-                int bottomC = hovered ? 168 : 128;
-                int topC = hovered ? 222 : 172;
-                
-                // Black Bar
-                buffer.begin(7, VertexFormats.POSITION_TEXTURE_COLOR);
-                buffer.vertex(scrollbarPositionMinX, this.getBounds().y + 1, 0.0D).texture(0, 1).color(0, 0, 0, 255).next();
-                buffer.vertex(scrollbarPositionMaxX, this.getBounds().y + 1, 0.0D).texture(1, 1).color(0, 0, 0, 255).next();
-                buffer.vertex(scrollbarPositionMaxX, getBounds().getMaxY() - 1, 0.0D).texture(1, 0).color(0, 0, 0, 255).next();
-                buffer.vertex(scrollbarPositionMinX, getBounds().getMaxY() - 1, 0.0D).texture(0, 0).color(0, 0, 0, 255).next();
-                tessellator.draw();
-                
-                // Bottom
-                buffer.begin(7, VertexFormats.POSITION_TEXTURE_COLOR);
-                buffer.vertex(scrollbarPositionMinX, minY + height, 0.0D).texture(0, 1).color(bottomC, bottomC, bottomC, 255).next();
-                buffer.vertex(scrollbarPositionMaxX, minY + height, 0.0D).texture(1, 1).color(bottomC, bottomC, bottomC, 255).next();
-                buffer.vertex(scrollbarPositionMaxX, minY, 0.0D).texture(1, 0).color(bottomC, bottomC, bottomC, 255).next();
-                buffer.vertex(scrollbarPositionMinX, minY, 0.0D).texture(0, 0).color(bottomC, bottomC, bottomC, 255).next();
-                tessellator.draw();
-                
-                // Top
-                buffer.begin(7, VertexFormats.POSITION_TEXTURE_COLOR);
-                buffer.vertex(scrollbarPositionMinX, (minY + height - 1), 0.0D).texture(0, 1).color(topC, topC, topC, 255).next();
-                buffer.vertex((scrollbarPositionMaxX - 1), (minY + height - 1), 0.0D).texture(1, 1).color(topC, topC, topC, 255).next();
-                buffer.vertex((scrollbarPositionMaxX - 1), minY, 0.0D).texture(1, 0).color(topC, topC, topC, 255).next();
-                buffer.vertex(scrollbarPositionMinX, minY, 0.0D).texture(0, 0).color(topC, topC, topC, 255).next();
-                tessellator.draw();
-            }
-        }
-        
-        private void updatePosition(float delta) {
-            double[] target = new double[]{this.target};
-            this.scroll = ClothConfigInitializer.handleScrollingPosition(target, this.scroll, this.getMaxScroll(), delta, this.start, this.duration);
-            this.target = target[0];
-        }
-        
         @Override
         public List<? extends Element> children() {
             return widgets;

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

@@ -27,15 +27,14 @@ import com.google.common.collect.Lists;
 import com.mojang.blaze3d.systems.RenderSystem;
 import me.shedaniel.clothconfig2.ClothConfigInitializer;
 import me.shedaniel.clothconfig2.api.ScissorsHandler;
-import me.shedaniel.clothconfig2.gui.widget.DynamicEntryListWidget;
 import me.shedaniel.math.Point;
 import me.shedaniel.math.Rectangle;
-import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.REIHelper;
 import me.shedaniel.rei.api.RecipeCategory;
 import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.entries.RecipeEntry;
+import me.shedaniel.rei.gui.widget.ScrollingContainer;
 import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.gui.widget.WidgetWithBounds;
 import me.shedaniel.rei.impl.RenderingEntry;
@@ -51,7 +50,6 @@ import net.minecraft.client.util.Texts;
 import net.minecraft.client.util.math.Matrix4f;
 import net.minecraft.text.Text;
 import net.minecraft.util.Identifier;
-import net.minecraft.util.math.MathHelper;
 import org.jetbrains.annotations.NotNull;
 
 import java.util.Collections;
@@ -133,10 +131,22 @@ public class DefaultInformationCategory implements RecipeCategory<DefaultInforma
     private static class ScrollableTextWidget extends WidgetWithBounds {
         private Rectangle bounds;
         private List<Text> texts;
-        private double target;
-        private double scroll;
-        private long start;
-        private long duration;
+        private final ScrollingContainer scrolling = new ScrollingContainer() {
+            @Override
+            public Rectangle getBounds() {
+                Rectangle bounds = ScrollableTextWidget.this.getBounds();
+                return new Rectangle(bounds.x + 1, bounds.y + 1, bounds.width - 2, bounds.height - 2);
+            }
+            
+            @Override
+            public int getMaxScrollHeight() {
+                int i = 2;
+                for (Text entry : texts) {
+                    i += entry == null ? 4 : font.fontHeight;
+                }
+                return i;
+            }
+        };
         
         public ScrollableTextWidget(Rectangle bounds, List<Text> texts) {
             this.bounds = Objects.requireNonNull(bounds);
@@ -151,48 +161,24 @@ public class DefaultInformationCategory implements RecipeCategory<DefaultInforma
         @Override
         public boolean mouseScrolled(double double_1, double double_2, double double_3) {
             if (containsMouse(double_1, double_2)) {
-                offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
+                scrolling.offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
                 return true;
             }
             return false;
         }
-        
-        public void offset(double value, boolean animated) {
-            scrollTo(target + value, animated);
-        }
-        
-        public void scrollTo(double value, boolean animated) {
-            scrollTo(value, animated, ClothConfigInitializer.getScrollDuration());
-        }
-        
-        public void scrollTo(double value, boolean animated, long duration) {
-            target = clamp(value);
-            
-            if (animated) {
-                start = System.currentTimeMillis();
-                this.duration = duration;
-            } else
-                scroll = target;
-        }
-        
-        public final double clamp(double v) {
-            return clamp(v, DynamicEntryListWidget.SmoothScrollingSettings.CLAMP_EXTENSION);
-        }
-        
-        public final double clamp(double v, double clampExtension) {
-            return MathHelper.clamp(v, -clampExtension, getMaxScroll() + clampExtension);
-        }
-        
-        protected int getMaxScroll() {
-            return Math.max(0, this.getMaxScrollPosition() - this.getBounds().height + 4);
+    
+        @Override
+        public boolean mouseClicked(double mouseX, double mouseY, int button) {
+            if (scrolling.updateDraggingState(mouseX, mouseY, button))
+                return true;
+            return super.mouseClicked(mouseX, mouseY, button);
         }
-        
-        protected int getMaxScrollPosition() {
-            int i = 0;
-            for (Text entry : texts) {
-                i += entry == null ? 4 : font.fontHeight;
-            }
-            return i;
+    
+        @Override
+        public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
+            if (scrolling.mouseDragged(mouseX, mouseY, button, deltaX, deltaY))
+                return true;
+            return super.mouseDragged(mouseX, mouseY, button, deltaX, deltaY);
         }
         
         @NotNull
@@ -203,10 +189,10 @@ public class DefaultInformationCategory implements RecipeCategory<DefaultInforma
         
         @Override
         public void render(int mouseX, int mouseY, float delta) {
-            updatePosition(delta);
-            Rectangle innerBounds = new Rectangle(bounds.x + 1, bounds.y + 1, bounds.width - 7, bounds.height - 2);
+            scrolling.updatePosition(delta);
+            Rectangle innerBounds = scrolling.getScissorBounds();
             ScissorsHandler.INSTANCE.scissor(innerBounds);
-            int currentY = (int) -scroll + innerBounds.y;
+            int currentY = (int) -scrolling.scrollAmount + innerBounds.y;
             for (Text text : texts) {
                 if (text != null && currentY + font.fontHeight >= innerBounds.y && currentY <= innerBounds.getMaxY()) {
                     font.draw(text.asFormattedString(), innerBounds.x + 2, currentY + 2, REIHelper.getInstance().isDarkThemeEnabled() ? 0xFFBBBBBB : 0xFF090909);
@@ -214,70 +200,11 @@ public class DefaultInformationCategory implements RecipeCategory<DefaultInforma
                 currentY += text == null ? 4 : font.fontHeight;
             }
             ScissorsHandler.INSTANCE.removeLastScissor();
-            ScissorsHandler.INSTANCE.scissor(bounds);
-            RenderSystem.enableBlend();
-            RenderSystem.blendFuncSeparate(770, 771, 0, 1);
-            RenderSystem.disableAlphaTest();
-            RenderSystem.shadeModel(7425);
-            RenderSystem.disableTexture();
-            renderScrollBar();
-            RenderSystem.enableTexture();
-            RenderSystem.shadeModel(7424);
-            RenderSystem.enableAlphaTest();
-            RenderSystem.disableBlend();
+            ScissorsHandler.INSTANCE.scissor(scrolling.getBounds());
+            scrolling.renderScrollBar(0xff000000, 1);
             ScissorsHandler.INSTANCE.removeLastScissor();
         }
         
-        @SuppressWarnings("deprecation")
-        private void renderScrollBar() {
-            int maxScroll = getMaxScroll();
-            int scrollbarPositionMinX = getBounds().getMaxX() - 7;
-            int scrollbarPositionMaxX = scrollbarPositionMinX + 6;
-            Tessellator tessellator = Tessellator.getInstance();
-            BufferBuilder buffer = tessellator.getBuffer();
-            if (maxScroll > 0) {
-                int height = (int) (((this.getBounds().height - 2f) * (this.getBounds().height - 2f)) / this.getMaxScrollPosition());
-                height = MathHelper.clamp(height, 32, this.getBounds().height - 2);
-                height -= Math.min((scroll < 0 ? (int) -scroll : scroll > maxScroll ? (int) scroll - maxScroll : 0), height * .95);
-                height = Math.max(10, height);
-                int minY = Math.min(Math.max((int) scroll * (this.getBounds().height - 2 - height) / maxScroll + getBounds().y + 1, getBounds().y + 1), getBounds().getMaxY() - 1 - height);
-                
-                boolean hovered = new Rectangle(scrollbarPositionMinX, minY, scrollbarPositionMaxX - scrollbarPositionMinX, height).contains(PointHelper.ofMouse());
-                int bottomC = hovered ? 168 : 128;
-                int topC = hovered ? 222 : 172;
-                
-                // Black Bar
-                buffer.begin(7, VertexFormats.POSITION_TEXTURE_COLOR);
-                buffer.vertex(scrollbarPositionMinX, this.getBounds().y + 1, 0.0D).texture(0, 1).color(0, 0, 0, 255).next();
-                buffer.vertex(scrollbarPositionMaxX, this.getBounds().y + 1, 0.0D).texture(1, 1).color(0, 0, 0, 255).next();
-                buffer.vertex(scrollbarPositionMaxX, getBounds().getMaxY() - 1, 0.0D).texture(1, 0).color(0, 0, 0, 255).next();
-                buffer.vertex(scrollbarPositionMinX, getBounds().getMaxY() - 1, 0.0D).texture(0, 0).color(0, 0, 0, 255).next();
-                tessellator.draw();
-                
-                // Bottom
-                buffer.begin(7, VertexFormats.POSITION_TEXTURE_COLOR);
-                buffer.vertex(scrollbarPositionMinX, minY + height, 0.0D).texture(0, 1).color(bottomC, bottomC, bottomC, 255).next();
-                buffer.vertex(scrollbarPositionMaxX, minY + height, 0.0D).texture(1, 1).color(bottomC, bottomC, bottomC, 255).next();
-                buffer.vertex(scrollbarPositionMaxX, minY, 0.0D).texture(1, 0).color(bottomC, bottomC, bottomC, 255).next();
-                buffer.vertex(scrollbarPositionMinX, minY, 0.0D).texture(0, 0).color(bottomC, bottomC, bottomC, 255).next();
-                tessellator.draw();
-                
-                // Top
-                buffer.begin(7, VertexFormats.POSITION_TEXTURE_COLOR);
-                buffer.vertex(scrollbarPositionMinX, (minY + height - 1), 0.0D).texture(0, 1).color(topC, topC, topC, 255).next();
-                buffer.vertex((scrollbarPositionMaxX - 1), (minY + height - 1), 0.0D).texture(1, 1).color(topC, topC, topC, 255).next();
-                buffer.vertex((scrollbarPositionMaxX - 1), minY, 0.0D).texture(1, 0).color(topC, topC, topC, 255).next();
-                buffer.vertex(scrollbarPositionMinX, minY, 0.0D).texture(0, 0).color(topC, topC, topC, 255).next();
-                tessellator.draw();
-            }
-        }
-        
-        private void updatePosition(float delta) {
-            double[] target = new double[]{this.target};
-            this.scroll = ClothConfigInitializer.handleScrollingPosition(target, this.scroll, this.getMaxScroll(), delta, this.start, this.duration);
-            this.target = target[0];
-        }
-        
         @Override
         public List<? extends Element> children() {
             return Collections.emptyList();

+ 6 - 0
src/main/resources/assets/roughlyenoughitems/lang/en_us.json

@@ -88,9 +88,13 @@
   "text.rei.recipe_screen_type.selection": "Recipe Screen Type Selection",
   "text.rei.recipe_screen_type.selection.sub": "You can always edit this setting again via the config screen.",
   "text.rei.view_recipes_for": "View Recipes for %s",
+  "text.rei.subsets": "Subsets",
+  "text.rei.tiny_potato": "Tiny Potatoz",
   "tooltip.rei.fluid_amount": "§7%d Unit",
   "msg.rei.copied_recipe_id": "Copied Recipe Identifier",
   "msg.rei.recipe_id_details": "Recipe ID: %s",
+  "subsets.rei.roughlyenoughitems.all_entries": "All Entries",
+  "subsets.rei.roughlyenoughitems.item_groups": "Creative Tabs",
   "_comment": "Config Tooltips",
   "config.roughlyenoughitems.title": "Roughly Enough Items Config",
   "config.roughlyenoughitems.!general": "General",
@@ -99,6 +103,7 @@
   "config.roughlyenoughitems.technical": "Technical",
   "config.roughlyenoughitems.performance": "Performance",
   "config.roughlyenoughitems.filtering": "Filtering",
+  "config.roughlyenoughitems.experimental": "Experimental Features",
   "config.roughlyenoughitems.cheating": "Cheating:",
   "config.roughlyenoughitems.smooth_scrolling": "Smooth Scrolling Settings",
   "config.roughlyenoughitems.recipeKeybind": "Show Recipe:",
@@ -111,6 +116,7 @@
   "config.roughlyenoughitems.favoriteKeybind": "Favorite Entry:",
   "config.roughlyenoughitems.favoritesEnabled": "Favorites Enabled:",
   "config.roughlyenoughitems.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:",