Explorar el Código

Favorites Dragging

Signed-off-by: shedaniel <daniel@shedaniel.me>
shedaniel hace 4 años
padre
commit
ada2fd1598
Se han modificado 35 ficheros con 2216 adiciones y 346 borrados
  1. 17 2
      RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/api/ConfigObject.java
  2. 12 9
      RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/api/EntryStack.java
  3. 109 0
      RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/api/favorites/FavoriteEntry.java
  4. 79 0
      RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/api/favorites/FavoriteEntryType.java
  5. 46 0
      RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/api/favorites/FavoriteMenuEntry.java
  6. 27 0
      RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/api/favorites/package-info.java
  7. 18 0
      RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/impl/Internals.java
  8. 11 3
      RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/utils/CollectionUtils.java
  9. 9 0
      RoughlyEnoughItems-default-plugin/src/main/java/me/shedaniel/rei/plugin/DefaultPlugin.java
  10. 97 0
      RoughlyEnoughItems-default-plugin/src/main/java/me/shedaniel/rei/plugin/favorites/Animator.java
  11. 273 0
      RoughlyEnoughItems-default-plugin/src/main/java/me/shedaniel/rei/plugin/favorites/GameModeFavoriteEntry.java
  12. 5 0
      RoughlyEnoughItems-runtime/build.gradle
  13. 106 2
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCore.java
  14. 59 0
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/compat/LBASupportPlugin.java
  15. 115 120
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/ContainerScreenOverlay.java
  16. 3 0
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/OverlaySearchField.java
  17. 0 4
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/RecipeViewingScreen.java
  18. 1 5
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/VillagerRecipeViewingScreen.java
  19. 5 1
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/modules/Menu.java
  20. 1 2
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/modules/entries/EntryStackSubsetsMenuEntry.java
  21. 1 1
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/modules/entries/GameModeMenuEntry.java
  22. 1 2
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/modules/entries/SubSubsetsMenuEntry.java
  23. 1 1
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/modules/entries/WeatherMenuEntry.java
  24. 124 0
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/plugin/DefaultRuntimePlugin.java
  25. 20 13
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/widget/EntryListEntryWidget.java
  26. 33 22
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/widget/EntryListWidget.java
  27. 16 12
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/widget/EntryWidget.java
  28. 781 134
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/widget/FavoritesListWidget.java
  29. 97 0
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/Animator.java
  30. 28 4
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/ConfigManagerImpl.java
  31. 9 8
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/ConfigObjectImpl.java
  32. 107 0
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/FavoriteEntryTypeRegistryImpl.java
  33. 1 0
      RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/RecipeHelperImpl.java
  34. 1 1
      gradle.properties
  35. 3 0
      src/main/resources/assets/roughlyenoughitems/lang/en_us.json

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

@@ -24,11 +24,13 @@
 package me.shedaniel.rei.api;
 
 import me.shedaniel.clothconfig2.api.ModifierKeyCode;
+import me.shedaniel.rei.api.favorites.FavoriteEntry;
 import me.shedaniel.rei.gui.config.*;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
 import org.jetbrains.annotations.ApiStatus;
 
+import java.util.Collections;
 import java.util.List;
 
 @Environment(EnvType.CLIENT)
@@ -57,6 +59,8 @@ public interface ConfigObject {
     
     boolean isGrabbingItems();
     
+    boolean isReducedMotion();
+    
     boolean isToastDisplayedOnCopyIdentifier();
     
     @Deprecated
@@ -123,7 +127,11 @@ public interface ConfigObject {
     
     boolean doDebugRenderTimeRequired();
     
-    boolean doSearchFavorites();
+    @Deprecated
+    @ApiStatus.ScheduledForRemoval
+    default boolean doSearchFavorites() {
+        return false;
+    }
     
     ModifierKeyCode getFavoriteKeyCode();
     
@@ -149,7 +157,14 @@ public interface ConfigObject {
     
     boolean isLowerConfigButton();
     
-    List<EntryStack> getFavorites();
+    @Deprecated
+    @ApiStatus.ScheduledForRemoval
+    default List<EntryStack> getFavorites() {
+        return Collections.emptyList();
+    }
+    
+    @ApiStatus.Experimental
+    List<FavoriteEntry> getFavoriteEntries();
     
     List<EntryStack> getFilteredStacks();
     

+ 12 - 9
RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/api/EntryStack.java

@@ -27,8 +27,8 @@ import com.google.common.collect.ImmutableList;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.mojang.blaze3d.vertex.PoseStack;
-import it.unimi.dsi.fastutil.shorts.Short2ObjectMap;
-import it.unimi.dsi.fastutil.shorts.Short2ObjectOpenHashMap;
+import com.mojang.serialization.Dynamic;
+import com.mojang.serialization.JsonOps;
 import me.shedaniel.math.Point;
 import me.shedaniel.math.Rectangle;
 import me.shedaniel.rei.api.fluid.FluidSupportProvider;
@@ -40,9 +40,11 @@ import net.fabricmc.api.Environment;
 import net.minecraft.client.resources.language.I18n;
 import net.minecraft.core.Registry;
 import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.NbtOps;
 import net.minecraft.nbt.TagParser;
 import net.minecraft.network.chat.Component;
 import net.minecraft.resources.ResourceLocation;
+import net.minecraft.util.GsonHelper;
 import net.minecraft.world.item.Item;
 import net.minecraft.world.item.ItemStack;
 import net.minecraft.world.item.crafting.Ingredient;
@@ -166,9 +168,11 @@ public interface EntryStack extends TextRepresentable {
     static EntryStack readFromJson(JsonElement jsonElement) {
         try {
             JsonObject obj = jsonElement.getAsJsonObject();
-            switch (obj.getAsJsonPrimitive("type").getAsString()) {
+            switch (GsonHelper.getAsString(obj, "type")) {
                 case "stack":
                     return EntryStack.create(ItemStack.of(TagParser.parseTag(obj.get("nbt").getAsString())));
+                case "item":
+                    return EntryStack.create(ItemStack.of((CompoundTag) Dynamic.convert(JsonOps.INSTANCE, NbtOps.INSTANCE, obj)));
                 case "fluid":
                     return EntryStack.create(Registry.FLUID.get(ResourceLocation.tryParse(obj.get("id").getAsString())));
                 case "empty":
@@ -188,9 +192,8 @@ public interface EntryStack extends TextRepresentable {
         try {
             switch (getType()) {
                 case ITEM:
-                    JsonObject obj1 = new JsonObject();
-                    obj1.addProperty("type", "stack");
-                    obj1.addProperty("nbt", getItemStack().save(new CompoundTag()).toString());
+                    JsonObject obj1 = Dynamic.convert(NbtOps.INSTANCE, JsonOps.INSTANCE, getItemStack().save(new CompoundTag())).getAsJsonObject();
+                    obj1.addProperty("type", "item");
                     return obj1;
                 case FLUID:
                     Optional<ResourceLocation> optionalIdentifier = getIdentifier();
@@ -368,7 +371,7 @@ public interface EntryStack extends TextRepresentable {
     
     class Settings<T> {
         @ApiStatus.Internal
-        private static final Short2ObjectMap<Settings<?>> ID_TO_SETTINGS = new Short2ObjectOpenHashMap<>();
+        private static final List<Settings<?>> SETTINGS = new ArrayList<>();
         
         public static final Supplier<Boolean> TRUE = () -> true;
         public static final Supplier<Boolean> FALSE = () -> false;
@@ -389,12 +392,12 @@ public interface EntryStack extends TextRepresentable {
         public Settings(T defaultValue) {
             this.defaultValue = defaultValue;
             this.id = nextId++;
-            ID_TO_SETTINGS.put(this.id, this);
+            SETTINGS.add(this);
         }
         
         @ApiStatus.Internal
         public static <T> Settings<T> getById(short id) {
-            return (Settings<T>) ID_TO_SETTINGS.get(id);
+            return (Settings<T>) SETTINGS.get(id);
         }
         
         public T getDefaultValue() {

+ 109 - 0
RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/api/favorites/FavoriteEntry.java

@@ -0,0 +1,109 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.api.favorites;
+
+import com.google.gson.JsonObject;
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.impl.Internals;
+import net.minecraft.resources.ResourceLocation;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collection;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.function.Supplier;
+
+public abstract class FavoriteEntry {
+    public static final String TYPE_KEY = "type";
+    private final UUID uuid = UUID.randomUUID();
+    
+    @NotNull
+    public static FavoriteEntry delegate(@NotNull Supplier<FavoriteEntry> supplier, @Nullable Supplier<JsonObject> toJson) {
+        return Internals.delegateFavoriteEntry(supplier, toJson);
+    }
+    
+    @Nullable
+    public static FavoriteEntry fromJson(@NotNull JsonObject object) {
+        return Internals.favoriteEntryFromJson(object);
+    }
+    
+    @NotNull
+    public static FavoriteEntry fromEntryStack(@NotNull EntryStack stack) {
+        return delegate(() -> FavoriteEntryType.registry().get(FavoriteEntryType.ENTRY_STACK).fromArgs(stack), null);
+    }
+    
+    public static boolean isEntryInvalid(@Nullable FavoriteEntry entry) {
+        return entry == null || entry.isInvalid();
+    }
+    
+    @NotNull
+    public JsonObject toJson(@NotNull JsonObject object) {
+        object.addProperty(TYPE_KEY, getType().toString());
+        return Objects.requireNonNull(Objects.requireNonNull(FavoriteEntryType.registry().get(getType())).toJson(this, object));
+    }
+    
+    public UUID getUuid() {
+        return uuid;
+    }
+    
+    public abstract boolean isInvalid();
+    
+    public abstract EntryStack getWidget(boolean showcase);
+    
+    public abstract boolean doAction(int button);
+    
+    @NotNull
+    public Optional<Supplier<Collection<@NotNull FavoriteMenuEntry>>> getMenuEntries() {
+        return Optional.empty();
+    }
+    
+    public abstract int hashIgnoreAmount();
+    
+    public abstract FavoriteEntry copy();
+    
+    public abstract ResourceLocation getType();
+    
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof FavoriteEntry)) return false;
+        FavoriteEntry that = (FavoriteEntry) o;
+        FavoriteEntry unwrapped = getUnwrapped();
+        FavoriteEntry thatUnwrapped = that.getUnwrapped();
+        return unwrapped == thatUnwrapped || unwrapped.isSame(thatUnwrapped);
+    }
+    
+    @Override
+    public int hashCode() {
+        return hashIgnoreAmount();
+    }
+    
+    public abstract boolean isSame(FavoriteEntry other);
+    
+    public FavoriteEntry getUnwrapped() {
+        return this;
+    }
+}

+ 79 - 0
RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/api/favorites/FavoriteEntryType.java

@@ -0,0 +1,79 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.api.favorites;
+
+import com.google.gson.JsonObject;
+import me.shedaniel.rei.impl.Internals;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+
+public interface FavoriteEntryType<T extends FavoriteEntry> {
+    ResourceLocation ENTRY_STACK = new ResourceLocation("roughlyenoughitems", "entry_stack");
+    
+    @NotNull
+    static Registry registry() {
+        return Internals.getFavoriteEntryTypeRegistry();
+    }
+    
+    @NotNull
+    T fromJson(@NotNull JsonObject object);
+    
+    @NotNull
+    T fromArgs(Object... args);
+    
+    @NotNull
+    JsonObject toJson(@NotNull T entry, @NotNull JsonObject object);
+    
+    @ApiStatus.NonExtendable
+    interface Registry {
+        void register(ResourceLocation id, FavoriteEntryType<?> type);
+        
+        @Nullable <A extends FavoriteEntry> FavoriteEntryType<A> get(ResourceLocation id);
+        
+        @Nullable
+        ResourceLocation getId(FavoriteEntryType<?> type);
+        
+        @NotNull
+        Section getOrCrateSection(Component text);
+        
+        @NotNull
+        Iterable<Section> sections();
+    }
+    
+    @ApiStatus.NonExtendable
+    interface Section {
+        void add(@NotNull FavoriteEntry... entries);
+        
+        @NotNull
+        Component getText();
+        
+        @NotNull
+        List<FavoriteEntry> getEntries();
+    }
+}

+ 46 - 0
RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/api/favorites/FavoriteMenuEntry.java

@@ -0,0 +1,46 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.api.favorites;
+
+import me.shedaniel.rei.gui.widget.Widget;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+public abstract class FavoriteMenuEntry extends Widget {
+    @Nullable
+    @ApiStatus.Internal
+    public Runnable closeMenu = null;
+    
+    public abstract int getEntryWidth();
+    
+    public abstract int getEntryHeight();
+    
+    public abstract void updateInformation(int xPos, int yPos, boolean selected, boolean containsMouse, boolean rendering, int width);
+    
+    public void closeMenu() {
+        if (closeMenu != null) {
+            closeMenu.run();
+        }
+    }
+}

+ 27 - 0
RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/api/favorites/package-info.java

@@ -0,0 +1,27 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+@ApiStatus.Experimental
+package me.shedaniel.rei.api.favorites;
+
+import org.jetbrains.annotations.ApiStatus;

+ 18 - 0
RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/impl/Internals.java

@@ -23,9 +23,12 @@
 
 package me.shedaniel.rei.impl;
 
+import com.google.gson.JsonObject;
 import me.shedaniel.math.Point;
 import me.shedaniel.math.Rectangle;
 import me.shedaniel.rei.api.*;
+import me.shedaniel.rei.api.favorites.FavoriteEntry;
+import me.shedaniel.rei.api.favorites.FavoriteEntryType;
 import me.shedaniel.rei.api.fluid.FluidSupportProvider;
 import me.shedaniel.rei.api.fractions.Fraction;
 import me.shedaniel.rei.api.subsets.SubsetsRegistry;
@@ -61,6 +64,9 @@ public final class Internals {
     private static Supplier<DisplayHelper> displayHelper = Internals::throwNotSetup;
     private static Supplier<WidgetsProvider> widgetsProvider = Internals::throwNotSetup;
     private static Supplier<ClientHelper.ViewSearchBuilder> viewSearchBuilder = Internals::throwNotSetup;
+    private static Supplier<FavoriteEntryType.Registry> favoriteEntryTypeRegistry = Internals::throwNotSetup;
+    private static BiFunction<Supplier<FavoriteEntry>, Supplier<JsonObject>, FavoriteEntry> delegateFavoriteEntry = (supplier, toJson) -> throwNotSetup();
+    private static Function<JsonObject, FavoriteEntry> favoriteEntryFromJson = (object) -> throwNotSetup();
     private static Function<@NotNull Boolean, ClickAreaHandler.Result> clickAreaHandlerResult = (result) -> throwNotSetup();
     private static BiFunction<@Nullable Point, Collection<Component>, Tooltip> tooltipProvider = (point, texts) -> throwNotSetup();
     private static Supplier<BuiltinPlugin> builtinPlugin = Internals::throwNotSetup;
@@ -125,6 +131,10 @@ public final class Internals {
         return viewSearchBuilder.get();
     }
     
+    public static FavoriteEntryType.Registry getFavoriteEntryTypeRegistry() {
+        return favoriteEntryTypeRegistry.get();
+    }
+    
     @NotNull
     public static ClickAreaHandler.Result createClickAreaHandlerResult(boolean applicable) {
         return clickAreaHandlerResult.apply(applicable);
@@ -161,6 +171,14 @@ public final class Internals {
         }
     }
     
+    public static FavoriteEntry delegateFavoriteEntry(Supplier<FavoriteEntry> supplier, Supplier<JsonObject> toJoin) {
+        return delegateFavoriteEntry.apply(supplier, toJoin);
+    }
+    
+    public static FavoriteEntry favoriteEntryFromJson(JsonObject object) {
+        return favoriteEntryFromJson.apply(object);
+    }
+    
     public interface EntryStackProvider {
         EntryStack empty();
         

+ 11 - 3
RoughlyEnoughItems-api/src/main/java/me/shedaniel/rei/utils/CollectionUtils.java

@@ -26,15 +26,15 @@ package me.shedaniel.rei.utils;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.common.collect.UnmodifiableIterator;
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+import it.unimi.dsi.fastutil.ints.IntList;
 import me.shedaniel.rei.api.EntryStack;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
 import net.minecraft.util.Mth;
 
 import java.util.*;
-import java.util.function.Function;
-import java.util.function.Predicate;
-import java.util.function.Supplier;
+import java.util.function.*;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -162,6 +162,14 @@ public class CollectionUtils {
         return l;
     }
     
+    public static <T> IntList mapToInt(Collection<T> list, ToIntFunction<T> function) {
+        IntList l = new IntArrayList(list.size() + 1);
+        for (T t : list) {
+            l.add(function.applyAsInt(t));
+        }
+        return l;
+    }
+    
     public static <T, R> List<R> mapParallel(Collection<T> list, Function<T, R> function) {
         return list.parallelStream().map(function).collect(Collectors.toList());
     }

+ 9 - 0
RoughlyEnoughItems-default-plugin/src/main/java/me/shedaniel/rei/plugin/DefaultPlugin.java

@@ -31,6 +31,8 @@ import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
 import it.unimi.dsi.fastutil.objects.ReferenceSet;
 import me.shedaniel.math.Rectangle;
 import me.shedaniel.rei.api.*;
+import me.shedaniel.rei.api.favorites.FavoriteEntry;
+import me.shedaniel.rei.api.favorites.FavoriteEntryType;
 import me.shedaniel.rei.api.fluid.FluidSupportProvider;
 import me.shedaniel.rei.api.fractions.Fraction;
 import me.shedaniel.rei.api.plugins.REIPluginV0;
@@ -52,6 +54,7 @@ import me.shedaniel.rei.plugin.crafting.DefaultCraftingCategory;
 import me.shedaniel.rei.plugin.crafting.DefaultCustomDisplay;
 import me.shedaniel.rei.plugin.crafting.DefaultShapedDisplay;
 import me.shedaniel.rei.plugin.crafting.DefaultShapelessDisplay;
+import me.shedaniel.rei.plugin.favorites.GameModeFavoriteEntry;
 import me.shedaniel.rei.plugin.fuel.DefaultFuelCategory;
 import me.shedaniel.rei.plugin.fuel.DefaultFuelDisplay;
 import me.shedaniel.rei.plugin.information.DefaultInformationCategory;
@@ -79,6 +82,7 @@ import net.minecraft.client.gui.screens.inventory.*;
 import net.minecraft.client.gui.screens.recipebook.RecipeUpdateListener;
 import net.minecraft.core.Registry;
 import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.TranslatableComponent;
 import net.minecraft.resources.ResourceLocation;
 import net.minecraft.tags.BlockTags;
 import net.minecraft.tags.ItemTags;
@@ -91,6 +95,7 @@ import net.minecraft.world.item.alchemy.PotionUtils;
 import net.minecraft.world.item.crafting.*;
 import net.minecraft.world.item.enchantment.Enchantment;
 import net.minecraft.world.item.enchantment.EnchantmentHelper;
+import net.minecraft.world.level.GameType;
 import net.minecraft.world.level.ItemLike;
 import net.minecraft.world.level.block.ComposterBlock;
 import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
@@ -418,6 +423,10 @@ public class DefaultPlugin implements REIPluginV0, BuiltinPlugin {
 //        SubsetsRegistry subsetsRegistry = SubsetsRegistry.INSTANCE;
 //        subsetsRegistry.registerPathEntry("roughlyenoughitems:food", EntryStack.create(Items.MILK_BUCKET));
 //        subsetsRegistry.registerPathEntry("roughlyenoughitems:food/roughlyenoughitems:cookies", EntryStack.create(Items.COOKIE));
+    
+        FavoriteEntryType.registry().register(GameModeFavoriteEntry.ID, GameModeFavoriteEntry.Type.INSTANCE);
+        FavoriteEntryType.registry().getOrCrateSection(new TranslatableComponent(GameModeFavoriteEntry.TRANSLATION_KEY))
+                .add(Arrays.stream(GameType.values()).<FavoriteEntry>map(GameModeFavoriteEntry.Type.INSTANCE::fromArgs).toArray(FavoriteEntry[]::new));
     }
     
     @Override

+ 97 - 0
RoughlyEnoughItems-default-plugin/src/main/java/me/shedaniel/rei/plugin/favorites/Animator.java

@@ -0,0 +1,97 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.plugin.favorites;
+
+import me.shedaniel.clothconfig2.api.ScrollingContainer;
+import net.minecraft.Util;
+import org.jetbrains.annotations.ApiStatus;
+
+@ApiStatus.Internal
+public final class Animator extends Number {
+    private double amount;
+    private double target;
+    private long start;
+    private long duration;
+    
+    public Animator() {
+    }
+    
+    public Animator(double amount) {
+        setAs(amount);
+    }
+    
+    public void setAs(double value) {
+        this.set(value, false, 0);
+    }
+    
+    public void setTo(double value, long duration) {
+        if (target != value)
+            this.set(value, true, duration);
+    }
+    
+    private void set(double value, boolean animated, long duration) {
+        this.target = value;
+        this.start = Util.getMillis();
+        
+        if (animated) {
+            this.duration = duration;
+        } else {
+            this.duration = 0;
+            this.amount = this.target;
+        }
+    }
+    
+    public void update(double delta) {
+        if (duration != 0) {
+            if (amount < target)
+                this.amount = Math.min(ScrollingContainer.ease(amount, target + (target - amount), Math.min(((double) Util.getMillis() - start) / duration * delta * 3.0D, 1.0D), v -> v), target);
+            else if (amount > target)
+                this.amount = Math.max(ScrollingContainer.ease(amount, target - (amount - target), Math.min(((double) Util.getMillis() - start) / duration * delta * 3.0D, 1.0D), v -> v), target);
+        }
+    }
+    
+    @Override
+    public int intValue() {
+        return (int) amount;
+    }
+    
+    @Override
+    public long longValue() {
+        return (long) amount;
+    }
+    
+    @Override
+    public float floatValue() {
+        return (float) amount;
+    }
+    
+    @Override
+    public double doubleValue() {
+        return amount;
+    }
+    
+    public double target() {
+        return target;
+    }
+}

+ 273 - 0
RoughlyEnoughItems-default-plugin/src/main/java/me/shedaniel/rei/plugin/favorites/GameModeFavoriteEntry.java

@@ -0,0 +1,273 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.plugin.favorites;
+
+import com.google.gson.JsonObject;
+import com.mojang.blaze3d.vertex.PoseStack;
+import me.shedaniel.clothconfig2.api.ScissorsHandler;
+import me.shedaniel.math.Point;
+import me.shedaniel.math.Rectangle;
+import me.shedaniel.rei.api.ConfigObject;
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.api.REIHelper;
+import me.shedaniel.rei.api.favorites.FavoriteEntry;
+import me.shedaniel.rei.api.favorites.FavoriteEntryType;
+import me.shedaniel.rei.api.favorites.FavoriteMenuEntry;
+import me.shedaniel.rei.api.widgets.Tooltip;
+import me.shedaniel.rei.impl.RenderingEntry;
+import me.shedaniel.rei.utils.CollectionUtils;
+import net.minecraft.ChatFormatting;
+import net.minecraft.Util;
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.Font;
+import net.minecraft.client.gui.components.events.GuiEventListener;
+import net.minecraft.client.resources.sounds.SimpleSoundInstance;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.TranslatableComponent;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.sounds.SoundEvents;
+import net.minecraft.util.GsonHelper;
+import net.minecraft.world.level.GameType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+import java.util.function.Supplier;
+
+public class GameModeFavoriteEntry extends FavoriteEntry {
+    public static final ResourceLocation ID = new ResourceLocation("roughlyenoughitems", "gamemode");
+    public static final String TRANSLATION_KEY = "favorite.section.gamemode";
+    public static final String KEY = "mode";
+    private GameType gameMode;
+    
+    public GameModeFavoriteEntry(GameType gameMode) {
+        this.gameMode = Objects.requireNonNull(gameMode);
+    }
+    
+    @Override
+    public boolean isInvalid() {
+        return false;
+    }
+    
+    @Override
+    public EntryStack getWidget(boolean showcase) {
+        return new RenderingEntry() {
+            private Animator notSetOffset = new Animator(0);
+            private Rectangle notSetScissorArea = new Rectangle();
+            private long nextSwitch = -1;
+            
+            @Override
+            public void render(PoseStack matrices, Rectangle bounds, int mouseX, int mouseY, float delta) {
+                int color = bounds.contains(mouseX, mouseY) ? 0xFFEEEEEE : 0xFFAAAAAA;
+                fillGradient(matrices, bounds.getX(), bounds.getY(), bounds.getMaxX(), bounds.getY() + 1, color, color);
+                fillGradient(matrices, bounds.getX(), bounds.getMaxY() - 1, bounds.getMaxX(), bounds.getMaxY(), color, color);
+                fillGradient(matrices, bounds.getX(), bounds.getY(), bounds.getX() + 1, bounds.getMaxY(), color, color);
+                fillGradient(matrices, bounds.getMaxX() - 1, bounds.getY(), bounds.getMaxX(), bounds.getMaxY(), color, color);
+                if (bounds.width > 4 && bounds.height > 4) {
+                    if (gameMode == GameType.NOT_SET) {
+                        updateAnimator(delta);
+                        notSetScissorArea.setBounds(bounds.x + 2, bounds.y + 2, bounds.width - 4, bounds.height - 4);
+                        ScissorsHandler.INSTANCE.scissor(notSetScissorArea);
+                        int offset = Math.round(notSetOffset.floatValue() * bounds.getHeight());
+                        for (int i = 0; i <= 3; i++) {
+                            GameType type = GameType.byId(i);
+                            renderGameModeText(matrices, type, bounds.getCenterX(), bounds.getCenterY() + bounds.getHeight() * i - offset, color);
+                        }
+                        ScissorsHandler.INSTANCE.removeLastScissor();
+                    } else {
+                        renderGameModeText(matrices, gameMode, bounds.getCenterX(), bounds.getCenterY(), color);
+                    }
+                }
+            }
+            
+            private void updateAnimator(float delta) {
+                notSetOffset.update(delta);
+                if (showcase) {
+                    if (nextSwitch == -1) {
+                        nextSwitch = Util.getMillis();
+                    }
+                    if (Util.getMillis() - nextSwitch > 1000) {
+                        nextSwitch = Util.getMillis();
+                        notSetOffset.setTo(((int) notSetOffset.target() + 1) % 4, 500);
+                    }
+                } else {
+                    notSetOffset.setTo((Minecraft.getInstance().gameMode.getPlayerMode().getId() + 1) % 4, 500);
+                }
+            }
+            
+            private void renderGameModeText(PoseStack matrices, GameType type, int centerX, int centerY, int color) {
+                Component s = new TranslatableComponent("text.rei.short_gamemode." + type.getName());
+                Font font = Minecraft.getInstance().font;
+                font.draw(matrices, s, centerX - font.width(s) / 2 + (type == GameType.NOT_SET ? 0 : 0.5f), centerY - 3.5f, color);
+            }
+            
+            @Override
+            public @Nullable Tooltip getTooltip(Point mouse) {
+                if (gameMode == GameType.NOT_SET)
+                    return Tooltip.create(mouse, new TranslatableComponent("text.rei.gamemode_button.tooltip.all"));
+                return Tooltip.create(mouse, new TranslatableComponent("text.rei.gamemode_button.tooltip.entry", gameMode.getDisplayName().getString()));
+            }
+        };
+    }
+    
+    @Override
+    public boolean doAction(int button) {
+        if (button == 0) {
+            GameType type = gameMode;
+            if (type == GameType.NOT_SET) {
+                type = GameType.byId(Minecraft.getInstance().gameMode.getPlayerMode().getId() + 1 % 4);
+            }
+            Minecraft.getInstance().player.chat(ConfigObject.getInstance().getGamemodeCommand().replaceAll("\\{gamemode}", type.name().toLowerCase(Locale.ROOT)));
+            Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F));
+            return true;
+        }
+        return false;
+    }
+    
+    @Override
+    public @NotNull Optional<Supplier<Collection<@NotNull FavoriteMenuEntry>>> getMenuEntries() {
+        if (gameMode == GameType.NOT_SET)
+            return Optional.of(this::_getMenuEntries);
+        return Optional.empty();
+    }
+    
+    private Collection<FavoriteMenuEntry> _getMenuEntries() {
+        return CollectionUtils.filterAndMap(Arrays.asList(GameType.values()), mode -> mode != GameType.NOT_SET, GameModeMenuEntry::new);
+    }
+    
+    @Override
+    public int hashIgnoreAmount() {
+        return gameMode.ordinal();
+    }
+    
+    @Override
+    public FavoriteEntry copy() {
+        return this;
+    }
+    
+    @Override
+    public ResourceLocation getType() {
+        return ID;
+    }
+    
+    @Override
+    public boolean isSame(FavoriteEntry other) {
+        if (!(other instanceof GameModeFavoriteEntry)) return false;
+        GameModeFavoriteEntry that = (GameModeFavoriteEntry) other;
+        return Objects.equals(gameMode, that.gameMode);
+    }
+    
+    public enum Type implements FavoriteEntryType<GameModeFavoriteEntry> {
+        INSTANCE;
+        
+        @Override
+        public @NotNull GameModeFavoriteEntry fromJson(@NotNull JsonObject object) {
+            return new GameModeFavoriteEntry(GameType.valueOf(GsonHelper.getAsString(object, KEY)));
+        }
+        
+        @Override
+        public @NotNull GameModeFavoriteEntry fromArgs(Object... args) {
+            return new GameModeFavoriteEntry((GameType) args[0]);
+        }
+        
+        @Override
+        public @NotNull JsonObject toJson(@NotNull GameModeFavoriteEntry entry, @NotNull JsonObject object) {
+            object.addProperty(KEY, entry.gameMode.name());
+            return object;
+        }
+    }
+    
+    public static class GameModeMenuEntry extends FavoriteMenuEntry {
+        public final String text;
+        public final GameType gameMode;
+        private int x, y, width;
+        private boolean selected, containsMouse, rendering;
+        private int textWidth = -69;
+        
+        public GameModeMenuEntry(GameType gameMode) {
+            this.text = gameMode.getDisplayName().getString();
+            this.gameMode = gameMode;
+        }
+        
+        private int getTextWidth() {
+            if (textWidth == -69) {
+                this.textWidth = Math.max(0, font.width(text));
+            }
+            return this.textWidth;
+        }
+        
+        @Override
+        public int getEntryWidth() {
+            return getTextWidth() + 4;
+        }
+        
+        @Override
+        public int getEntryHeight() {
+            return 12;
+        }
+        
+        @Override
+        public List<? extends GuiEventListener> children() {
+            return Collections.emptyList();
+        }
+        
+        @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(PoseStack matrices, int mouseX, int mouseY, float delta) {
+            boolean disabled = this.minecraft.gameMode.getPlayerMode() == gameMode;
+            if (selected && !disabled) {
+                fill(matrices, x, y, x + width, y + 12, -12237499);
+            }
+            if (!disabled && selected && containsMouse) {
+                REIHelper.getInstance().queueTooltip(Tooltip.create(new TranslatableComponent("text.rei.gamemode_button.tooltip.entry", text)));
+            }
+            String s = text;
+            if (disabled) {
+                s = ChatFormatting.STRIKETHROUGH.toString() + s;
+            }
+            font.draw(matrices, s, x + 2, y + 2, selected && !disabled ? 16777215 : 8947848);
+        }
+        
+        @Override
+        public boolean mouseClicked(double mouseX, double mouseY, int button) {
+            boolean disabled = this.minecraft.gameMode.getPlayerMode() == gameMode;
+            if (!disabled && rendering && mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + 12) {
+                Minecraft.getInstance().player.chat(ConfigObject.getInstance().getGamemodeCommand().replaceAll("\\{gamemode}", gameMode.name().toLowerCase(Locale.ROOT)));
+                minecraft.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F));
+                closeMenu();
+                return true;
+            }
+            return super.mouseClicked(mouseX, mouseY, button);
+        }
+    }
+}

+ 5 - 0
RoughlyEnoughItems-runtime/build.gradle

@@ -1,9 +1,14 @@
 archivesBaseName = "RoughlyEnoughItems-runtime"
 
+repositories {
+    maven { url "https://mod-buildcraft.com/maven" }
+}
+
 loom {
     accessWidener = file("src/main/resources/roughlyenoughitems-runtime.accessWidener")
 }
 
 dependencies {
     compile project(path: ':RoughlyEnoughItems-api', configuration: 'dev')
+    modCompileOnly("alexiil.mc.lib:libblockattributes-fluids:0.8.3-pre.3")
 }

+ 106 - 2
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCore.java

@@ -25,11 +25,17 @@ package me.shedaniel.rei;
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
 import me.shedaniel.cloth.api.client.events.v0.ClothClientHooks;
+import me.shedaniel.clothconfig2.api.LazyResettable;
 import me.shedaniel.math.Point;
 import me.shedaniel.math.Rectangle;
 import me.shedaniel.math.api.Executor;
 import me.shedaniel.rei.api.*;
+import me.shedaniel.rei.api.favorites.FavoriteEntry;
+import me.shedaniel.rei.api.favorites.FavoriteEntryType;
+import me.shedaniel.rei.api.favorites.FavoriteMenuEntry;
 import me.shedaniel.rei.api.fluid.FluidSupportProvider;
 import me.shedaniel.rei.api.fractions.Fraction;
 import me.shedaniel.rei.api.plugins.REIPluginV0;
@@ -66,6 +72,7 @@ import net.minecraft.network.chat.Component;
 import net.minecraft.network.chat.FormattedText;
 import net.minecraft.network.chat.TextComponent;
 import net.minecraft.resources.ResourceLocation;
+import net.minecraft.util.GsonHelper;
 import net.minecraft.world.InteractionResult;
 import net.minecraft.world.inventory.CraftingMenu;
 import net.minecraft.world.inventory.Slot;
@@ -77,6 +84,7 @@ import net.minecraft.world.level.material.Fluid;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import java.io.File;
@@ -88,6 +96,7 @@ import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.function.BiFunction;
 import java.util.function.Function;
+import java.util.function.Supplier;
 import java.util.stream.Stream;
 
 import static me.shedaniel.rei.impl.Internals.attachInstance;
@@ -180,6 +189,94 @@ public class RoughlyEnoughItemsCore implements ClientModInitializer {
                 return new FillRectangleDrawableConsumer(rectangle, color);
             }
         }, Internals.WidgetsProvider.class);
+        attachInstance((Supplier<FavoriteEntryType.Registry>) FavoriteEntryTypeRegistryImpl::getInstance, "favoriteEntryTypeRegistry");
+        attachInstance((BiFunction<Supplier<FavoriteEntry>, Supplier<JsonObject>, FavoriteEntry>) (supplier, toJson) -> new FavoriteEntry() {
+            LazyResettable<FavoriteEntry> value = new LazyResettable<>(supplier);
+            
+            @Override
+            public FavoriteEntry getUnwrapped() {
+                FavoriteEntry entry = value.get();
+                if (entry == null) {
+                    value.reset();
+                }
+                return Objects.requireNonNull(entry).getUnwrapped();
+            }
+    
+            @Override
+            public UUID getUuid() {
+                return getUnwrapped().getUuid();
+            }
+    
+            @Override
+            public boolean isInvalid() {
+                try {
+                    return getUnwrapped().isInvalid();
+                } catch (Exception e) {
+                    return true;
+                }
+            }
+            
+            @Override
+            public EntryStack getWidget(boolean showcase) {
+                return getUnwrapped().getWidget(showcase);
+            }
+            
+            @Override
+            public boolean doAction(int button) {
+                return getUnwrapped().doAction(button);
+            }
+    
+            @Override
+            public @NotNull Optional<Supplier<Collection<@NotNull FavoriteMenuEntry>>> getMenuEntries() {
+                return getUnwrapped().getMenuEntries();
+            }
+    
+            @Override
+            public int hashIgnoreAmount() {
+                return getUnwrapped().hashIgnoreAmount();
+            }
+            
+            @Override
+            public FavoriteEntry copy() {
+                return FavoriteEntry.delegate(supplier, toJson);
+            }
+            
+            @Override
+            public ResourceLocation getType() {
+                return getUnwrapped().getType();
+            }
+            
+            @Override
+            public @NotNull JsonObject toJson(@NotNull JsonObject to) {
+                if (toJson == null) {
+                    return getUnwrapped().toJson(to);
+                }
+                
+                JsonObject object = toJson.get();
+                for (Map.Entry<String, JsonElement> entry : object.entrySet()) {
+                    to.add(entry.getKey(), entry.getValue());
+                }
+                return to;
+            }
+            
+            @Override
+            public boolean isSame(FavoriteEntry other) {
+                return getUnwrapped().isSame(other.getUnwrapped());
+            }
+        }, "delegateFavoriteEntry");
+        attachInstance((Function<JsonObject, FavoriteEntry>) (object) -> {
+            String type = GsonHelper.getAsString(object, FavoriteEntry.TYPE_KEY);
+            switch (type) {
+                case "stack":
+                case "item":
+                case "fluid":
+                case "empty":
+                    return FavoriteEntry.fromEntryStack(EntryStack.readFromJson(object));
+                default:
+                    ResourceLocation id = new ResourceLocation(type);
+                    return Objects.requireNonNull(Objects.requireNonNull(FavoriteEntryType.registry().get(id)).fromJson(object));
+            }
+        }, "favoriteEntryFromJson");
         attachInstance((BiFunction<@Nullable Point, Collection<Component>, Tooltip>) QueuedTooltip::create, "tooltipProvider");
         attachInstance((Function<@Nullable Boolean, ClickAreaHandler.Result>) successful -> new ClickAreaHandler.Result() {
             private List<ResourceLocation> categories = Lists.newArrayList();
@@ -189,12 +286,12 @@ public class RoughlyEnoughItemsCore implements ClientModInitializer {
                 this.categories.add(category);
                 return this;
             }
-    
+            
             @Override
             public boolean isSuccessful() {
                 return successful;
             }
-    
+            
             @Override
             public Stream<ResourceLocation> getCategories() {
                 return categories.stream();
@@ -387,6 +484,13 @@ public class RoughlyEnoughItemsCore implements ClientModInitializer {
         if (isDebugModeEnabled()) {
             registerPlugin(new REITestPlugin());
         }
+        if (FabricLoader.getInstance().isModLoaded("libblockattributes-fluids")) {
+            try {
+                registerPlugin((REIPluginEntry) Class.forName("me.shedaniel.rei.compat.LBASupportPlugin").getConstructor().newInstance());
+            } catch (Throwable throwable) {
+                throwable.printStackTrace();
+            }
+        }
     }
     
     private boolean shouldReturn(Screen screen) {

+ 59 - 0
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/compat/LBASupportPlugin.java

@@ -0,0 +1,59 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.compat;
+
+import alexiil.mc.lib.attributes.fluid.FluidAttributes;
+import alexiil.mc.lib.attributes.fluid.GroupedFluidInvView;
+import alexiil.mc.lib.attributes.fluid.amount.FluidAmount;
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.api.RecipeHelper;
+import me.shedaniel.rei.api.fluid.FluidSupportProvider;
+import me.shedaniel.rei.api.fractions.Fraction;
+import me.shedaniel.rei.api.plugins.REIPluginV0;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.InteractionResultHolder;
+
+import java.util.stream.Stream;
+
+public class LBASupportPlugin implements REIPluginV0 {
+    @Override
+    public ResourceLocation getPluginIdentifier() {
+        return new ResourceLocation("roughlyenoughitems", "lba_support");
+    }
+    
+    @Override
+    public void registerOthers(RecipeHelper recipeHelper) {
+        FluidSupportProvider.getInstance().registerProvider(itemStack -> {
+            GroupedFluidInvView view = FluidAttributes.GROUPED_INV_VIEW.get(itemStack.getItemStack());
+            if (view.getStoredFluids().size() > 0)
+                return InteractionResultHolder.success(view.getStoredFluids().stream()
+                        .filter(fluidKey -> !fluidKey.isEmpty() && fluidKey.getRawFluid() != null)
+                        .map(fluidKey -> {
+                            FluidAmount amount = view.getAmount_F(fluidKey);
+                            return EntryStack.create(fluidKey.getRawFluid(), Fraction.of(amount.whole, amount.numerator, amount.denominator));
+                        }));
+            return InteractionResultHolder.pass(Stream.empty());
+        });
+    }
+}

+ 115 - 120
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/ContainerScreenOverlay.java

@@ -35,6 +35,7 @@ import me.shedaniel.math.Rectangle;
 import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.*;
+import me.shedaniel.rei.api.favorites.FavoriteEntry;
 import me.shedaniel.rei.api.widgets.Button;
 import me.shedaniel.rei.api.widgets.Tooltip;
 import me.shedaniel.rei.api.widgets.Widgets;
@@ -72,11 +73,12 @@ import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import java.util.*;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 @ApiStatus.Internal
 public class ContainerScreenOverlay extends WidgetWithBounds implements REIOverlay {
-    
     private static final ResourceLocation CHEST_GUI_TEXTURE = new ResourceLocation("roughlyenoughitems", "textures/gui/recipecontainer.png");
     private static final List<Tooltip> TOOLTIPS = Lists.newArrayList();
     private static final List<Runnable> AFTER_RENDER = Lists.newArrayList();
@@ -120,22 +122,9 @@ public class ContainerScreenOverlay extends WidgetWithBounds implements REIOverl
     private Button leftButton, rightButton;
     @ApiStatus.Experimental
     private Rectangle subsetsButtonBounds;
-    @ApiStatus.Experimental
-    @Nullable
-    private Menu subsetsMenu = null;
-    private Widget wrappedSubsetsMenu = null;
     
     @Nullable
-    private Menu weatherMenu = null;
-    private Widget wrappedWeatherMenu = null;
-    private boolean renderWeatherMenu = false;
-    private Button weatherButton = null;
-    
-    @Nullable
-    private Menu gameModeMenu = null;
-    private Widget wrappedGameModeMenu = null;
-    private boolean renderGameModeMenu = false;
-    private Button gameModeButton = null;
+    private ContainerScreenOverlay.OverlayMenu overlayMenu = null;
     
     public static EntryListWidget getEntryListWidget() {
         return ENTRY_LIST_WIDGET;
@@ -146,26 +135,67 @@ public class ContainerScreenOverlay extends WidgetWithBounds implements REIOverl
         return favoritesListWidget;
     }
     
-    @ApiStatus.Experimental
-    @Nullable
-    public Menu getSubsetsMenu() {
-        return subsetsMenu;
+    private static class OverlayMenu {
+        @NotNull
+        private UUID uuid;
+        @NotNull
+        private Menu menu;
+        @NotNull
+        private Widget wrappedMenu;
+        @NotNull
+        private Predicate<Point> inBounds;
+        
+        public OverlayMenu(@NotNull UUID uuid, @NotNull Menu menu, @NotNull Widget wrappedMenu, @NotNull Predicate<Point> inBounds) {
+            this.uuid = uuid;
+            this.menu = menu;
+            this.wrappedMenu = wrappedMenu;
+            this.inBounds = inBounds.or(point -> menu.getBounds().contains(point));
+        }
+    }
+    
+    public boolean isMenuOpened(UUID uuid) {
+        return overlayMenu != null && overlayMenu.uuid.equals(uuid);
+    }
+    
+    public boolean isAnyMenuOpened() {
+        return overlayMenu != null;
     }
     
-    public void removeWeatherMenu() {
-        this.renderWeatherMenu = false;
-        Widget tmpWeatherMenu = wrappedWeatherMenu;
-        AFTER_RENDER.add(() -> this.widgets.remove(tmpWeatherMenu));
-        this.weatherMenu = null;
-        this.wrappedWeatherMenu = null;
+    public boolean isMenuInBounds(UUID uuid) {
+        return isMenuOpened(uuid) && overlayMenu.inBounds.test(PointHelper.ofMouse());
     }
     
-    public void removeGameModeMenu() {
-        this.renderGameModeMenu = false;
-        Widget tmpGameModeMenu = wrappedGameModeMenu;
-        AFTER_RENDER.add(() -> this.widgets.remove(tmpGameModeMenu));
-        this.gameModeMenu = null;
-        this.wrappedGameModeMenu = null;
+    private void proceedOpenMenu(UUID uuid, Runnable runnable) {
+        proceedOpenMenuOrElse(uuid, runnable, menu -> {});
+    }
+    
+    private void proceedOpenMenuOrElse(UUID uuid, Runnable runnable, Consumer<OverlayMenu> orElse) {
+        if (overlayMenu == null || !overlayMenu.uuid.equals(uuid)) {
+            removeOverlayMenu();
+            runnable.run();
+        } else {
+            orElse.accept(this.overlayMenu);
+        }
+    }
+    
+    public void openMenu(UUID uuid, Menu menu, Predicate<Point> inPoint) {
+        this.overlayMenu = new OverlayMenu(uuid, menu, InternalWidgets.wrapTranslate(menu, 0, 0, 400), inPoint);
+    }
+    
+    @ApiStatus.Internal
+    @Nullable
+    public Menu getSubsetsMenu() {
+        if (isMenuOpened(Menu.SUBSETS))
+            return this.overlayMenu.menu;
+        throw new IllegalStateException("Subsets menu accessed when subsets are not opened!");
+    }
+    
+    @ApiStatus.Internal
+    public void removeOverlayMenu() {
+        OverlayMenu tmpOverlayMenu = this.overlayMenu;
+        if (tmpOverlayMenu != null)
+            AFTER_RENDER.add(() -> this.widgets.remove(tmpOverlayMenu.wrappedMenu));
+        this.overlayMenu = null;
     }
     
     @Override
@@ -181,17 +211,14 @@ public class ContainerScreenOverlay extends WidgetWithBounds implements REIOverl
         this.shouldReInit = false;
         //Update Variables
         this.children().clear();
-        this.wrappedSubsetsMenu = null;
-        this.subsetsMenu = null;
-        this.weatherMenu = null;
-        this.renderWeatherMenu = false;
-        this.weatherButton = null;
+        this.removeOverlayMenu();
         this.window = Minecraft.getInstance().getWindow();
         this.bounds = DisplayHelper.getInstance().getOverlayBounds(ConfigObject.getInstance().getDisplayPanelLocation(), Minecraft.getInstance().screen);
         widgets.add(ENTRY_LIST_WIDGET);
         if (ConfigObject.getInstance().isFavoritesEnabled()) {
             if (favoritesListWidget == null)
                 favoritesListWidget = new FavoritesListWidget();
+//            favoritesListWidget.favoritePanel.resetRows();
             widgets.add(favoritesListWidget);
         }
         ENTRY_LIST_WIDGET.updateArea(ScreenHelper.getSearchField() == null ? "" : null);
@@ -268,20 +295,21 @@ public class ContainerScreenOverlay extends WidgetWithBounds implements REIOverl
         ));
         tmp.setZ(600);
         if (ConfigObject.getInstance().doesShowUtilsButtons()) {
-            widgets.add(gameModeButton = Widgets.createButton(ConfigObject.getInstance().isLowerConfigButton() ? new Rectangle(ConfigObject.getInstance().isLeftHandSidePanel() ? window.getGuiScaledWidth() - 30 : 10, 10, 20, 20) : new Rectangle(ConfigObject.getInstance().isLeftHandSidePanel() ? window.getGuiScaledWidth() - 55 : 35, 10, 20, 20), NarratorChatListener.NO_TITLE)
+            widgets.add(Widgets.createButton(ConfigObject.getInstance().isLowerConfigButton() ? new Rectangle(ConfigObject.getInstance().isLeftHandSidePanel() ? window.getGuiScaledWidth() - 30 : 10, 10, 20, 20) : new Rectangle(ConfigObject.getInstance().isLeftHandSidePanel() ? window.getGuiScaledWidth() - 55 : 35, 10, 20, 20), NarratorChatListener.NO_TITLE)
                     .onRender((matrices, button) -> {
-                        boolean tmpRender = renderGameModeMenu;
-                        renderGameModeMenu = !renderWeatherMenu && (button.isFocused() || button.containsMouse(PointHelper.ofMouse()) || (wrappedGameModeMenu != null && wrappedGameModeMenu.containsMouse(PointHelper.ofMouse())));
-                        if (tmpRender != renderGameModeMenu) {
-                            if (renderGameModeMenu) {
-                                this.gameModeMenu = new Menu(new Point(button.getBounds().x, button.getBounds().getMaxY()),
-                                        CollectionUtils.filterAndMap(Arrays.asList(GameType.values()), mode -> mode != GameType.NOT_SET, GameModeMenuEntry::new));
-                                if (ConfigObject.getInstance().isLeftHandSidePanel())
-                                    this.gameModeMenu.menuStartPoint.x -= this.gameModeMenu.getBounds().width - this.gameModeButton.getBounds().width;
-                                this.wrappedGameModeMenu = InternalWidgets.wrapTranslate(InternalWidgets.wrapLateRenderable(gameModeMenu), 0, 0, 600);
-                                AFTER_RENDER.add(() -> this.widgets.add(wrappedGameModeMenu));
-                            } else {
-                                removeGameModeMenu();
+                        boolean isOpened = isMenuOpened(Menu.GAME_TYPE);
+                        if (isOpened || !isAnyMenuOpened()) {
+                            boolean inBounds = (button.isFocused() || button.containsMouse(PointHelper.ofMouse())) || isMenuInBounds(Menu.GAME_TYPE);
+                            if (isOpened != inBounds) {
+                                if (inBounds) {
+                                    Menu menu = new Menu(new Point(button.getBounds().x, button.getBounds().getMaxY()),
+                                            CollectionUtils.filterAndMap(Arrays.asList(GameType.values()), mode -> mode != GameType.NOT_SET, GameModeMenuEntry::new));
+                                    if (ConfigObject.getInstance().isLeftHandSidePanel())
+                                        menu.menuStartPoint.x -= menu.getBounds().width - button.getBounds().width;
+                                    openMenu(Menu.GAME_TYPE, menu, point -> button.isFocused() && button.containsMouse(PointHelper.ofMouse()));
+                                } else {
+                                    removeOverlayMenu();
+                                }
                             }
                         }
                         button.setText(new TextComponent(getGameModeShortText(getCurrentGameMode())));
@@ -289,20 +317,22 @@ public class ContainerScreenOverlay extends WidgetWithBounds implements REIOverl
                     .focusable(false)
                     .tooltipLine(I18n.get("text.rei.gamemode_button.tooltip.all"))
                     .containsMousePredicate((button, point) -> button.getBounds().contains(point) && isNotInExclusionZones(point.x, point.y)));
+            Button weatherButton;
             widgets.add(weatherButton = Widgets.createButton(new Rectangle(ConfigObject.getInstance().isLeftHandSidePanel() ? window.getGuiScaledWidth() - 30 : 10, 35, 20, 20), NarratorChatListener.NO_TITLE)
                     .onRender((matrices, button) -> {
-                        boolean tmpRender = renderWeatherMenu;
-                        renderWeatherMenu = !renderGameModeMenu && (button.isFocused() || button.containsMouse(PointHelper.ofMouse()) || (wrappedWeatherMenu != null && wrappedWeatherMenu.containsMouse(PointHelper.ofMouse())));
-                        if (tmpRender != renderWeatherMenu) {
-                            if (renderWeatherMenu) {
-                                this.weatherMenu = new Menu(new Point(button.getBounds().x, button.getBounds().getMaxY()),
-                                        CollectionUtils.map(Weather.values(), WeatherMenuEntry::new));
-                                if (ConfigObject.getInstance().isLeftHandSidePanel())
-                                    this.weatherMenu.menuStartPoint.x -= this.weatherMenu.getBounds().width - this.weatherButton.getBounds().width;
-                                this.wrappedWeatherMenu = InternalWidgets.wrapTranslate(InternalWidgets.wrapLateRenderable(weatherMenu), 0, 0, 400);
-                                AFTER_RENDER.add(() -> this.widgets.add(wrappedWeatherMenu));
-                            } else {
-                                removeWeatherMenu();
+                        boolean isOpened = isMenuOpened(Menu.WEATHER);
+                        if (isOpened || !isAnyMenuOpened()) {
+                            boolean inBounds = (button.isFocused() || button.containsMouse(PointHelper.ofMouse())) || isMenuInBounds(Menu.WEATHER);
+                            if (isOpened != inBounds) {
+                                if (inBounds) {
+                                    Menu menu = new Menu(new Point(button.getBounds().x, button.getBounds().getMaxY()),
+                                            CollectionUtils.map(Weather.values(), WeatherMenuEntry::new));
+                                    if (ConfigObject.getInstance().isLeftHandSidePanel())
+                                        menu.menuStartPoint.x -= menu.getBounds().width - button.getBounds().width;
+                                    openMenu(Menu.WEATHER, menu, point -> button.isFocused() && button.containsMouse(PointHelper.ofMouse()));
+                                } else {
+                                    removeOverlayMenu();
+                                }
                             }
                         }
                     })
@@ -319,14 +349,11 @@ public class ContainerScreenOverlay extends WidgetWithBounds implements REIOverl
         if (ConfigObject.getInstance().isSubsetsEnabled()) {
             widgets.add(InternalWidgets.wrapLateRenderable(InternalWidgets.wrapTranslate(Widgets.createButton(subsetsButtonBounds, ClientHelperImpl.getInstance().isAprilFools.get() ? new TranslatableComponent("text.rei.tiny_potato") : new TranslatableComponent("text.rei.subsets"))
                     .onClick(button -> {
-                        if (subsetsMenu == null) {
-                            wrappedSubsetsMenu = InternalWidgets.wrapTranslate(InternalWidgets.wrapLateRenderable(this.subsetsMenu = Menu.createSubsetsMenuFromRegistry(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;
-                        }
+                        proceedOpenMenuOrElse(Menu.SUBSETS, () -> {
+                            openMenu(Menu.SUBSETS, Menu.createSubsetsMenuFromRegistry(new Point(this.subsetsButtonBounds.x, this.subsetsButtonBounds.getMaxY())), point -> true);
+                        }, menu -> {
+                            removeOverlayMenu();
+                        });
                     }), 0, 0, 600)));
         }
         if (!ConfigObject.getInstance().isEntryListWidgetScrolled()) {
@@ -559,24 +586,15 @@ public class ContainerScreenOverlay extends WidgetWithBounds implements REIOverl
         if (ScreenHelper.isOverlayVisible()) {
             ScreenHelper.getSearchField().laterRender(matrices, mouseX, mouseY, delta);
             for (Widget widget : widgets) {
-                if (widget instanceof LateRenderable && wrappedSubsetsMenu != widget && wrappedWeatherMenu != widget && wrappedGameModeMenu != widget)
+                if (widget instanceof LateRenderable && (overlayMenu == null || overlayMenu.wrappedMenu != widget))
                     widget.render(matrices, mouseX, mouseY, delta);
             }
         }
-        if (wrappedWeatherMenu != null) {
-            if (wrappedWeatherMenu.containsMouse(mouseX, mouseY)) {
+        if (overlayMenu != null) {
+            if (overlayMenu.wrappedMenu.containsMouse(mouseX, mouseY)) {
                 TOOLTIPS.clear();
             }
-            wrappedWeatherMenu.render(matrices, mouseX, mouseY, delta);
-        } else if (wrappedGameModeMenu != null) {
-            if (wrappedGameModeMenu.containsMouse(mouseX, mouseY)) {
-                TOOLTIPS.clear();
-            }
-            wrappedGameModeMenu.render(matrices, mouseX, mouseY, delta);
-        }
-        if (wrappedSubsetsMenu != null) {
-            TOOLTIPS.clear();
-            wrappedSubsetsMenu.render(matrices, mouseX, mouseY, delta);
+            overlayMenu.wrappedMenu.render(matrices, mouseX, mouseY, delta);
         }
         Screen currentScreen = Minecraft.getInstance().screen;
         if (!(currentScreen instanceof RecipeViewingScreen) || !((RecipeViewingScreen) currentScreen).choosePageActivated)
@@ -633,11 +651,7 @@ public class ContainerScreenOverlay extends WidgetWithBounds implements REIOverl
     public boolean mouseScrolled(double mouseX, double mouseY, double amount) {
         if (!ScreenHelper.isOverlayVisible())
             return false;
-        if (wrappedSubsetsMenu != null && wrappedSubsetsMenu.mouseScrolled(mouseX, mouseY, amount))
-            return true;
-        if (wrappedWeatherMenu != null && wrappedWeatherMenu.mouseScrolled(mouseX, mouseY, amount))
-            return true;
-        if (wrappedGameModeMenu != null && wrappedGameModeMenu.mouseScrolled(mouseX, mouseY, amount))
+        if (overlayMenu != null && overlayMenu.wrappedMenu.mouseScrolled(mouseX, mouseY, amount))
             return true;
         if (isInside(PointHelper.ofMouse())) {
             if (!ConfigObject.getInstance().isEntryListWidgetScrolled()) {
@@ -657,9 +671,7 @@ public class ContainerScreenOverlay extends WidgetWithBounds implements REIOverl
         }
         for (Widget widget : widgets)
             if (widget != ENTRY_LIST_WIDGET && (favoritesListWidget == null || widget != favoritesListWidget)
-                && (wrappedSubsetsMenu == null || widget != wrappedSubsetsMenu)
-                && (wrappedWeatherMenu == null || widget != wrappedWeatherMenu)
-                && (wrappedGameModeMenu == null || widget != wrappedGameModeMenu)
+                && (overlayMenu == null || widget != overlayMenu.wrappedMenu)
                 && widget.mouseScrolled(mouseX, mouseY, amount))
                 return true;
         return false;
@@ -686,9 +698,9 @@ public class ContainerScreenOverlay extends WidgetWithBounds implements REIOverl
             } else if (ConfigObject.getInstance().getUsageKeybind().matchesKey(keyCode, scanCode)) {
                 return ClientHelper.getInstance().openView(ClientHelper.ViewSearchBuilder.builder().addUsagesFor(stack).setInputNotice(stack).fillPreferredOpenedCategory());
             } else if (ConfigObject.getInstance().getFavoriteKeyCode().matchesKey(keyCode, scanCode)) {
-                stack.setAmount(127);
-                if (!CollectionUtils.anyMatchEqualsEntryIgnoreAmount(ConfigObject.getInstance().getFavorites(), stack))
-                    ConfigObject.getInstance().getFavorites().add(stack);
+                FavoriteEntry favoriteEntry = FavoriteEntry.fromEntryStack(stack);
+                if (!ConfigObject.getInstance().getFavoriteEntries().contains(favoriteEntry))
+                    ConfigObject.getInstance().getFavoriteEntries().add(favoriteEntry);
                 ConfigManager.getInstance().saveConfig();
                 FavoritesListWidget favoritesListWidget = ContainerScreenOverlay.getFavoritesListWidget();
                 if (favoritesListWidget != null)
@@ -739,9 +751,9 @@ public class ContainerScreenOverlay extends WidgetWithBounds implements REIOverl
             } else if (ConfigObject.getInstance().getUsageKeybind().matchesMouse(button)) {
                 return ClientHelper.getInstance().openView(ClientHelper.ViewSearchBuilder.builder().addUsagesFor(stack).setInputNotice(stack).fillPreferredOpenedCategory());
             } else if (ConfigObject.getInstance().getFavoriteKeyCode().matchesMouse(button)) {
-                stack.setAmount(127);
-                if (!CollectionUtils.anyMatchEqualsEntryIgnoreAmount(ConfigObject.getInstance().getFavorites(), stack))
-                    ConfigObject.getInstance().getFavorites().add(stack);
+                FavoriteEntry favoriteEntry = FavoriteEntry.fromEntryStack(stack);
+                if (!ConfigObject.getInstance().getFavoriteEntries().contains(favoriteEntry))
+                    ConfigObject.getInstance().getFavoriteEntries().add(favoriteEntry);
                 ConfigManager.getInstance().saveConfig();
                 FavoritesListWidget favoritesListWidget = ContainerScreenOverlay.getFavoritesListWidget();
                 if (favoritesListWidget != null)
@@ -751,33 +763,16 @@ public class ContainerScreenOverlay extends WidgetWithBounds implements REIOverl
         }
         if (!ScreenHelper.isOverlayVisible())
             return false;
-        if (wrappedSubsetsMenu != null && wrappedSubsetsMenu.mouseClicked(mouseX, mouseY, button)) {
-            this.setFocused(wrappedSubsetsMenu);
-            if (button == 0)
-                this.setDragging(true);
-            ScreenHelper.getSearchField().setFocused(false);
-            return true;
-        }
-        if (wrappedWeatherMenu != null) {
-            if (wrappedWeatherMenu.mouseClicked(mouseX, mouseY, button)) {
-                this.setFocused(wrappedWeatherMenu);
-                if (button == 0)
-                    this.setDragging(true);
-                ScreenHelper.getSearchField().setFocused(false);
-                return true;
-            } else if (!wrappedWeatherMenu.containsMouse(mouseX, mouseY) && !weatherButton.containsMouse(mouseX, mouseY)) {
-                removeWeatherMenu();
-            }
-        }
-        if (wrappedGameModeMenu != null) {
-            if (wrappedGameModeMenu.mouseClicked(mouseX, mouseY, button)) {
-                this.setFocused(wrappedGameModeMenu);
+        if (overlayMenu != null) {
+            if (overlayMenu.wrappedMenu.mouseClicked(mouseX, mouseY, button)) {
+                if (overlayMenu != null) this.setFocused(overlayMenu.wrappedMenu);
+                else this.setFocused(null);
                 if (button == 0)
                     this.setDragging(true);
                 ScreenHelper.getSearchField().setFocused(false);
                 return true;
-            } else if (!wrappedGameModeMenu.containsMouse(mouseX, mouseY) && !gameModeButton.containsMouse(mouseX, mouseY)) {
-                removeGameModeMenu();
+            } else if (!overlayMenu.inBounds.test(new Point(mouseX, mouseY))) {
+                removeOverlayMenu();
             }
         }
         if (ConfigObject.getInstance().areClickableRecipeArrowsEnabled()) {
@@ -811,7 +806,7 @@ public class ContainerScreenOverlay extends WidgetWithBounds implements REIOverl
             }
         }
         for (GuiEventListener element : widgets)
-            if (element != wrappedSubsetsMenu && element != wrappedWeatherMenu && element != wrappedGameModeMenu && element.mouseClicked(mouseX, mouseY, button)) {
+            if ((overlayMenu == null || element != overlayMenu.wrappedMenu) && element.mouseClicked(mouseX, mouseY, button)) {
                 this.setFocused(element);
                 if (button == 0)
                     this.setDragging(true);

+ 3 - 0
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/OverlaySearchField.java

@@ -84,10 +84,13 @@ public class OverlaySearchField extends TextFieldWidget {
     
     @Override
     protected void renderSuggestion(PoseStack matrices, int x, int y) {
+        matrices.pushPose();
+        matrices.translate(0, 0, 400);
         if (containsMouse(PointHelper.ofMouse()) || isFocused())
             this.font.drawShadow(matrices, this.font.plainSubstrByWidth(this.getSuggestion(), this.getWidth()), x, y, REIHelper.getInstance().isDarkThemeEnabled() ? 0xccddaa3d : 0xddeaeaea);
         else
             this.font.drawShadow(matrices, this.font.plainSubstrByWidth(this.getSuggestion(), this.getWidth()), x, y, -6250336);
+        matrices.popPose();
     }
     
     @Override

+ 0 - 4
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/RecipeViewingScreen.java

@@ -197,7 +197,6 @@ public class RecipeViewingScreen extends Screen implements RecipeScreen {
                 return true;
         if (keyCode == 256 || this.minecraft.options.keyInventory.matches(keyCode, scanCode)) {
             Minecraft.getInstance().setScreen(REIHelper.getInstance().getPreviousContainerScreen());
-            ScreenHelper.getLastOverlay().init();
             return true;
         }
         if (keyCode == 259) {
@@ -359,7 +358,6 @@ public class RecipeViewingScreen extends Screen implements RecipeScreen {
         }
         
         children.addAll(tabs);
-        children.add(ScreenHelper.getLastOverlay(true, false));
         children.addAll(widgets);
         children.addAll(preWidgets);
     }
@@ -428,8 +426,6 @@ public class RecipeViewingScreen extends Screen implements RecipeScreen {
             if (tab.isSelected())
                 tab.render(matrices, mouseX, mouseY, delta);
         }
-        ScreenHelper.getLastOverlay().render(matrices, mouseX, mouseY, delta);
-        ScreenHelper.getLastOverlay().lateRender(matrices, mouseX, mouseY, delta);
         {
             ModifierKeyCode export = ConfigObject.getInstance().getExportImageKeybind();
             if (export.matchesCurrentKey() || export.matchesCurrentMouse()) {

+ 1 - 5
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/VillagerRecipeViewingScreen.java

@@ -248,8 +248,6 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
         this.children.addAll(buttonList);
         this.widgets.addAll(tabs);
         this.children.addAll(widgets);
-        this.children.add(ScreenHelper.getLastOverlay(true, false));
-        ScreenHelper.getLastOverlay().init();
     }
     
     @Override
@@ -360,7 +358,7 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
         for (Widget widget : widgets) {
             widget.render(matrices, mouseX, mouseY, delta);
         }
-        ScreenHelper.getLastOverlay().render(matrices, mouseX, mouseY, delta);
+        super.render(matrices, mouseX, mouseY, delta);
         RenderSystem.pushMatrix();
         ScissorsHandler.INSTANCE.scissor(scrolling.getBounds());
         for (Button button : buttonList) {
@@ -380,7 +378,6 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
         scrolling.renderScrollBar(0, scrollBarAlpha, REIHelper.getInstance().isDarkThemeEnabled() ? 0.8f : 1f);
         ScissorsHandler.INSTANCE.removeLastScissor();
         RenderSystem.popMatrix();
-        ScreenHelper.getLastOverlay().lateRender(matrices, mouseX, mouseY, delta);
     }
     
     @Override
@@ -436,7 +433,6 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
                 return true;
         if (keyCode == 256 || this.minecraft.options.keyInventory.matches(keyCode, scanCode)) {
             Minecraft.getInstance().setScreen(REIHelper.getInstance().getPreviousContainerScreen());
-            ScreenHelper.getLastOverlay().init();
             return true;
         }
         if (keyCode == 259) {

+ 5 - 1
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/modules/Menu.java

@@ -56,6 +56,10 @@ import java.util.stream.Collectors;
 @ApiStatus.Experimental
 @ApiStatus.Internal
 public class Menu extends WidgetWithBounds implements LateRenderable {
+    public static final UUID SUBSETS = UUID.randomUUID();
+    public static final UUID WEATHER = UUID.randomUUID();
+    public static final UUID GAME_TYPE = UUID.randomUUID();
+    
     public final Point menuStartPoint;
     private final List<MenuEntry> entries = Lists.newArrayList();
     public final ScrollingContainer scrolling = new ScrollingContainer() {
@@ -245,7 +249,7 @@ public class Menu extends WidgetWithBounds implements LateRenderable {
     public boolean mouseClicked(double mouseX, double mouseY, int button) {
         if (scrolling.updateDraggingState(mouseX, mouseY, button))
             return true;
-        return super.mouseClicked(mouseX, mouseY, button);
+        return super.mouseClicked(mouseX, mouseY, button) || getInnerBounds().contains(mouseX, mouseY);
     }
     
     @Override

+ 1 - 2
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/modules/entries/EntryStackSubsetsMenuEntry.java

@@ -31,7 +31,6 @@ import me.shedaniel.rei.api.*;
 import me.shedaniel.rei.gui.ContainerScreenOverlay;
 import me.shedaniel.rei.gui.modules.Menu;
 import me.shedaniel.rei.gui.modules.MenuEntry;
-import me.shedaniel.rei.impl.EntryRegistryImpl;
 import me.shedaniel.rei.impl.ScreenHelper;
 import me.shedaniel.rei.utils.CollectionUtils;
 import net.minecraft.client.gui.components.events.GuiEventListener;
@@ -102,7 +101,7 @@ public class EntryStackSubsetsMenuEntry extends MenuEntry {
                     if (subsetsMenu != null)
                         recalculateFilter(subsetsMenu);
                     ConfigManager.getInstance().saveConfig();
-                    ((EntryRegistryImpl) EntryRegistry.getInstance()).refilter();
+                    EntryRegistry.getInstance().refilter();
                     if (ScreenHelper.getSearchField() != null)
                         ContainerScreenOverlay.getEntryListWidget().updateSearch(ScreenHelper.getSearchField().getText(), true);
                 }

+ 1 - 1
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/modules/entries/GameModeMenuEntry.java

@@ -100,7 +100,7 @@ public class GameModeMenuEntry extends MenuEntry {
         if (rendering && mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + 12) {
             Minecraft.getInstance().player.chat(ConfigObject.getInstance().getGamemodeCommand().replaceAll("\\{gamemode}", gameMode.name().toLowerCase(Locale.ROOT)));
             minecraft.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F));
-            ScreenHelper.getLastOverlay().removeGameModeMenu();
+            ScreenHelper.getLastOverlay().removeOverlayMenu();
             return true;
         }
         return super.mouseClicked(mouseX, mouseY, button);

+ 1 - 2
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/modules/entries/SubSubsetsMenuEntry.java

@@ -34,7 +34,6 @@ import me.shedaniel.rei.gui.ContainerScreenOverlay;
 import me.shedaniel.rei.gui.modules.Menu;
 import me.shedaniel.rei.gui.modules.MenuEntry;
 import me.shedaniel.rei.gui.widget.TabWidget;
-import me.shedaniel.rei.impl.EntryRegistryImpl;
 import me.shedaniel.rei.impl.ScreenHelper;
 import net.minecraft.client.Minecraft;
 import net.minecraft.client.gui.components.events.GuiEventListener;
@@ -152,7 +151,7 @@ public class SubSubsetsMenuEntry extends MenuEntry {
                 Menu subsetsMenu = ScreenHelper.getLastOverlay().getSubsetsMenu();
                 setFiltered(filteredStacks, subsetsMenu, this, !(getFilteredRatio() > 0));
                 ConfigManager.getInstance().saveConfig();
-                ((EntryRegistryImpl) EntryRegistry.getInstance()).refilter();
+                EntryRegistry.getInstance().refilter();
                 if (ScreenHelper.getSearchField() != null)
                     ContainerScreenOverlay.getEntryListWidget().updateSearch(ScreenHelper.getSearchField().getText(), true);
             } else {

+ 1 - 1
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/modules/entries/WeatherMenuEntry.java

@@ -101,7 +101,7 @@ public class WeatherMenuEntry extends MenuEntry {
         if (rendering && mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + 12) {
             Minecraft.getInstance().player.chat(ConfigObject.getInstance().getWeatherCommand().replaceAll("\\{weather}", weather.name().toLowerCase(Locale.ROOT)));
             minecraft.getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F));
-            ScreenHelper.getLastOverlay().removeWeatherMenu();
+            ScreenHelper.getLastOverlay().removeOverlayMenu();
             return true;
         }
         return super.mouseClicked(mouseX, mouseY, button);

+ 124 - 0
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/plugin/DefaultRuntimePlugin.java

@@ -23,27 +23,38 @@
 
 package me.shedaniel.rei.gui.plugin;
 
+import com.google.gson.JsonObject;
 import com.mojang.blaze3d.vertex.PoseStack;
 import me.shedaniel.math.Point;
 import me.shedaniel.math.Rectangle;
 import me.shedaniel.rei.api.*;
+import me.shedaniel.rei.api.favorites.FavoriteEntry;
+import me.shedaniel.rei.api.favorites.FavoriteEntryType;
 import me.shedaniel.rei.api.plugins.REIPluginV0;
 import me.shedaniel.rei.api.widgets.Panel;
 import me.shedaniel.rei.api.widgets.Tooltip;
+import me.shedaniel.rei.gui.ContainerScreenOverlay;
 import me.shedaniel.rei.gui.RecipeViewingScreen;
 import me.shedaniel.rei.gui.VillagerRecipeViewingScreen;
+import me.shedaniel.rei.gui.widget.FavoritesListWidget;
 import me.shedaniel.rei.impl.ClientHelperImpl;
 import me.shedaniel.rei.impl.RenderingEntry;
 import me.shedaniel.rei.plugin.autocrafting.DefaultCategoryHandler;
 import net.fabricmc.api.EnvType;
 import net.fabricmc.api.Environment;
 import net.minecraft.client.Minecraft;
+import net.minecraft.client.gui.screens.Screen;
 import net.minecraft.network.chat.TextComponent;
 import net.minecraft.resources.ResourceLocation;
+import net.minecraft.util.GsonHelper;
+import net.minecraft.world.InteractionResult;
+import net.minecraft.world.item.Item;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import java.util.Collections;
+import java.util.function.Function;
 
 @ApiStatus.Internal
 @Environment(EnvType.CLIENT)
@@ -87,6 +98,14 @@ public class DefaultRuntimePlugin implements REIPluginV0 {
                 return Collections.emptyList();
             return Collections.singletonList(widget.getBounds().clone());
         });
+        /*baseBoundsHandler.registerExclusionZones(Screen.class, () -> {
+            FavoritesListWidget widget = ContainerScreenOverlay.getFavoritesListWidget();
+            if (widget != null) {
+                if (widget.favoritePanelButton.isVisible())
+                    return Collections.singletonList(widget.favoritePanelButton.bounds);
+            }
+            return Collections.emptyList();
+        });*/
         displayHelper.registerProvider(new DisplayHelper.DisplayBoundsProvider<RecipeViewingScreen>() {
             @Override
             public Rectangle getScreenBounds(RecipeViewingScreen screen) {
@@ -97,6 +116,11 @@ public class DefaultRuntimePlugin implements REIPluginV0 {
             public Class<?> getBaseSupportedClass() {
                 return RecipeViewingScreen.class;
             }
+            
+            @Override
+            public InteractionResult shouldScreenBeOverlayed(Class<?> screen) {
+                return InteractionResult.SUCCESS;
+            }
         });
         displayHelper.registerProvider(new DisplayHelper.DisplayBoundsProvider<VillagerRecipeViewingScreen>() {
             @Override
@@ -108,11 +132,111 @@ public class DefaultRuntimePlugin implements REIPluginV0 {
             public Class<?> getBaseSupportedClass() {
                 return VillagerRecipeViewingScreen.class;
             }
+            
+            @Override
+            public InteractionResult shouldScreenBeOverlayed(Class<?> screen) {
+                return InteractionResult.SUCCESS;
+            }
         });
     }
     
     @Override
     public void registerOthers(RecipeHelper recipeHelper) {
         recipeHelper.registerAutoCraftingHandler(new DefaultCategoryHandler());
+        FavoriteEntryType.registry().register(EntryStackFavoriteType.INSTANCE.id, EntryStackFavoriteType.INSTANCE);
+    }
+    
+    private enum EntryStackFavoriteType implements FavoriteEntryType<EntryStackFavoriteEntry> {
+        INSTANCE(FavoriteEntryType.ENTRY_STACK);
+        
+        private final String key = "data";
+        private ResourceLocation id;
+        
+        EntryStackFavoriteType(ResourceLocation id) {
+            this.id = id;
+        }
+        
+        @Override
+        public @NotNull EntryStackFavoriteEntry fromJson(@NotNull JsonObject object) {
+            return new EntryStackFavoriteEntry(EntryStack.readFromJson(GsonHelper.getAsJsonObject(object, key)));
+        }
+        
+        @Override
+        public @NotNull EntryStackFavoriteEntry fromArgs(Object... args) {
+            return new EntryStackFavoriteEntry((EntryStack) args[0]);
+        }
+        
+        @Override
+        public @NotNull JsonObject toJson(@NotNull EntryStackFavoriteEntry entry, @NotNull JsonObject object) {
+            object.add(key, entry.stack.toJson());
+            return object;
+        }
+    }
+    
+    private static class EntryStackFavoriteEntry extends FavoriteEntry {
+        private static final Function<EntryStack, String> CANCEL_FLUID_AMOUNT = s -> null;
+        private final EntryStack stack;
+        private final int hashIgnoreAmount;
+        
+        public EntryStackFavoriteEntry(EntryStack stack) {
+            this.stack = stack.copy();
+            this.stack.setAmount(127);
+            if (this.stack.getType() == EntryStack.Type.ITEM)
+                this.stack.setting(EntryStack.Settings.RENDER_COUNTS, EntryStack.Settings.FALSE);
+            else if (this.stack.getType() == EntryStack.Type.ITEM)
+                this.stack.setting(EntryStack.Settings.Fluid.AMOUNT_TOOLTIP, CANCEL_FLUID_AMOUNT);
+            this.hashIgnoreAmount = stack.hashIgnoreAmount();
+        }
+        
+        @Override
+        public boolean isInvalid() {
+            return this.stack.isEmpty();
+        }
+        
+        @Override
+        public EntryStack getWidget(boolean showcase) {
+            return this.stack;
+        }
+    
+        @Override
+        public boolean doAction(int button) {
+            if (!ClientHelper.getInstance().isCheating()) return false;
+            EntryStack entry = stack.copy();
+            if (!entry.isEmpty()) {
+                if (entry.getType() == EntryStack.Type.FLUID) {
+                    Item bucketItem = entry.getFluid().getBucket();
+                    if (bucketItem != null) {
+                        entry = EntryStack.create(bucketItem);
+                    }
+                }
+                if (entry.getType() == EntryStack.Type.ITEM)
+                    entry.setAmount(button != 1 && !Screen.hasShiftDown() ? 1 : entry.getItemStack().getMaxStackSize());
+                return ClientHelper.getInstance().tryCheatingEntry(entry);
+            }
+    
+            return false;
+        }
+    
+        @Override
+        public int hashIgnoreAmount() {
+            return hashIgnoreAmount;
+        }
+        
+        @Override
+        public FavoriteEntry copy() {
+            return new EntryStackFavoriteEntry(stack.copy());
+        }
+        
+        @Override
+        public ResourceLocation getType() {
+            return EntryStackFavoriteType.INSTANCE.id;
+        }
+        
+        @Override
+        public boolean isSame(FavoriteEntry other) {
+            if (!(other instanceof EntryStackFavoriteEntry)) return false;
+            EntryStackFavoriteEntry that = (EntryStackFavoriteEntry) other;
+            return stack.equalsIgnoreAmount(that.stack);
+        }
     }
 }

+ 20 - 13
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/widget/EntryListEntryWidget.java

@@ -31,7 +31,7 @@ import me.shedaniel.rei.api.EntryStack;
 import net.minecraft.client.gui.screens.Screen;
 import net.minecraft.world.item.Item;
 
-public class EntryListEntryWidget extends EntryWidget {
+public abstract class EntryListEntryWidget extends EntryWidget {
     public int backupY;
     
     protected EntryListEntryWidget(Point point, int entrySize) {
@@ -58,21 +58,28 @@ public class EntryListEntryWidget extends EntryWidget {
     public boolean mouseReleased(double mouseX, double mouseY, int button) {
         if (!interactable)
             return super.mouseReleased(mouseX, mouseY, button);
-        if (containsMouse(mouseX, mouseY) && ClientHelper.getInstance().isCheating()) {
-            EntryStack entry = getCurrentEntry().copy();
-            if (!entry.isEmpty() && wasClicked()) {
-                if (entry.getType() == EntryStack.Type.FLUID) {
-                    Item bucketItem = entry.getFluid().getBucket();
-                    if (bucketItem != null) {
-                        entry = EntryStack.create(bucketItem);
-                    }
+        if (containsMouse(mouseX, mouseY) && wasClicked() && doAction(mouseX, mouseY, button)) {
+            return true;
+        }
+        return super.mouseReleased(mouseX, mouseY, button);
+    }
+    
+    protected boolean doAction(double mouseX, double mouseY, int button) {
+        if (!ClientHelper.getInstance().isCheating()) return false;
+        EntryStack entry = getCurrentEntry().copy();
+        if (!entry.isEmpty()) {
+            if (entry.getType() == EntryStack.Type.FLUID) {
+                Item bucketItem = entry.getFluid().getBucket();
+                if (bucketItem != null) {
+                    entry = EntryStack.create(bucketItem);
                 }
-                if (entry.getType() == EntryStack.Type.ITEM)
-                    entry.setAmount(button != 1 && !Screen.hasShiftDown() ? 1 : entry.getItemStack().getMaxStackSize());
-                return ClientHelper.getInstance().tryCheatingEntry(entry);
             }
+            if (entry.getType() == EntryStack.Type.ITEM)
+                entry.setAmount(button != 1 && !Screen.hasShiftDown() ? 1 : entry.getItemStack().getMaxStackSize());
+            return ClientHelper.getInstance().tryCheatingEntry(entry);
         }
-        return super.mouseReleased(mouseX, mouseY, button);
+        
+        return false;
     }
     
     @Override

+ 33 - 22
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/widget/EntryListWidget.java

@@ -24,6 +24,7 @@
 package me.shedaniel.rei.gui.widget;
 
 import com.google.common.base.Stopwatch;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.mojang.blaze3d.vertex.PoseStack;
 import com.mojang.blaze3d.vertex.Tesselator;
@@ -56,6 +57,8 @@ import net.minecraft.util.Mth;
 import net.minecraft.world.InteractionResult;
 import net.minecraft.world.item.CreativeModeTab;
 import net.minecraft.world.item.Item;
+import org.apache.commons.lang3.mutable.MutableInt;
+import org.apache.commons.lang3.mutable.MutableLong;
 import org.jetbrains.annotations.ApiStatus;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -187,24 +190,32 @@ public class EntryListWidget extends WidgetWithBounds {
         return Mth.ceil(allStacks.size() / (float) entries.size());
     }
     
-    public static void renderEntries(boolean debugTime, int[] size, long[] time, boolean fastEntryRendering, PoseStack matrices, int mouseX, int mouseY, float delta, List<EntryListEntryWidget> entries) {
-        if (entries.isEmpty()) return;
-        EntryListEntryWidget firstWidget = entries.get(0);
+    public static <T extends EntryListEntryWidget> void renderEntries(MutableInt size, MutableLong time, boolean fastEntryRendering, PoseStack matrices, int mouseX, int mouseY, float delta, Iterable<T> entries) {
+        renderEntries(true, size, time, fastEntryRendering, matrices, mouseX, mouseY, delta, entries);
+    }
+    
+    public static <T extends EntryListEntryWidget> void renderEntries(boolean fastEntryRendering, PoseStack matrices, int mouseX, int mouseY, float delta, Iterable<T> entries) {
+        renderEntries(false, null, null, fastEntryRendering, matrices, mouseX, mouseY, delta, entries);
+    }
+    
+    public static <T extends EntryListEntryWidget> void renderEntries(boolean debugTime, MutableInt size, MutableLong time, boolean fastEntryRendering, PoseStack matrices, int mouseX, int mouseY, float delta, Iterable<T> entries) {
+        T firstWidget = Iterables.getFirst(entries, null);
+        if (firstWidget == null) return;
         EntryStack first = firstWidget.getCurrentEntry();
         if (fastEntryRendering && first instanceof OptimalEntryStack) {
             OptimalEntryStack firstStack = (OptimalEntryStack) first;
             firstStack.optimisedRenderStart(matrices, delta);
             long l = debugTime ? System.nanoTime() : 0;
             MultiBufferSource.BufferSource immediate = Minecraft.getInstance().renderBuffers().bufferSource();
-            for (EntryListEntryWidget listEntry : entries) {
+            for (T listEntry : entries) {
                 EntryStack currentEntry = listEntry.getCurrentEntry();
                 currentEntry.setZ(100);
                 listEntry.drawBackground(matrices, mouseX, mouseY, delta);
                 ((OptimalEntryStack) currentEntry).optimisedRenderBase(matrices, immediate, listEntry.getInnerBounds(), mouseX, mouseY, delta);
-                if (debugTime && !currentEntry.isEmpty()) size[0]++;
+                if (debugTime && !currentEntry.isEmpty()) size.increment();
             }
             immediate.endBatch();
-            for (EntryListEntryWidget listEntry : entries) {
+            for (T listEntry : entries) {
                 EntryStack currentEntry = listEntry.getCurrentEntry();
                 ((OptimalEntryStack) currentEntry).optimisedRenderOverlay(matrices, listEntry.getInnerBounds(), mouseX, mouseY, delta);
                 if (listEntry.containsMouse(mouseX, mouseY)) {
@@ -212,17 +223,17 @@ public class EntryListWidget extends WidgetWithBounds {
                     listEntry.drawHighlighted(matrices, mouseX, mouseY, delta);
                 }
             }
-            if (debugTime) time[0] += (System.nanoTime() - l);
+            if (debugTime) time.add(System.nanoTime() - l);
             firstStack.optimisedRenderEnd(matrices, delta);
         } else {
-            for (EntryListEntryWidget entry : entries) {
+            for (T entry : entries) {
                 if (entry.getCurrentEntry().isEmpty())
                     continue;
                 if (debugTime) {
-                    size[0]++;
+                    size.increment();
                     long l = System.nanoTime();
                     entry.render(matrices, mouseX, mouseY, delta);
-                    time[0] += (System.nanoTime() - l);
+                    time.add(System.nanoTime() - l);
                 } else entry.render(matrices, mouseX, mouseY, delta);
             }
         }
@@ -230,8 +241,8 @@ public class EntryListWidget extends WidgetWithBounds {
     
     @Override
     public void render(PoseStack matrices, int mouseX, int mouseY, float delta) {
-        int[] size = {0};
-        long[] time = {0};
+        MutableInt size = new MutableInt();
+        MutableLong time = new MutableLong();
         long totalTimeStart = debugTime ? System.nanoTime() : 0;
         boolean fastEntryRendering = ConfigObject.getInstance().doesFastEntryRendering();
         if (ConfigObject.getInstance().isEntryListWidgetScrolled()) {
@@ -262,9 +273,9 @@ public class EntryListWidget extends WidgetWithBounds {
             }).limit(Math.max(0, allStacks.size() - i[0]));
             
             if (fastEntryRendering) {
-                entryStream.collect(Collectors.groupingBy(entryListEntry -> OptimalEntryStack.groupingHashFrom(entryListEntry.getCurrentEntry()))).forEach((integer, entries) -> {
-                    renderEntries(debugTime, size, time, fastEntryRendering, matrices, mouseX, mouseY, delta, (List) entries);
-                });
+                for (List<EntryListEntry> entries : entryStream.collect(Collectors.groupingBy(entryListEntry -> OptimalEntryStack.groupingHashFrom(entryListEntry.getCurrentEntry()))).values()) {
+                    renderEntries(debugTime, size, time, fastEntryRendering, matrices, mouseX, mouseY, delta, entries);
+                }
             } else {
                 renderEntries(debugTime, size, time, fastEntryRendering, matrices, mouseX, mouseY, delta, entryStream.collect(Collectors.toList()));
             }
@@ -278,16 +289,16 @@ public class EntryListWidget extends WidgetWithBounds {
             }
             if (fastEntryRendering) {
                 entries.stream().collect(Collectors.groupingBy(entryListEntry -> OptimalEntryStack.groupingHashFrom(entryListEntry.getCurrentEntry()))).forEach((integer, entries) -> {
-                    renderEntries(debugTime, size, time, fastEntryRendering, matrices, mouseX, mouseY, delta, (List) entries);
+                    renderEntries(debugTime, size, time, fastEntryRendering, matrices, mouseX, mouseY, delta, entries);
                 });
             } else {
-                renderEntries(debugTime, size, time, fastEntryRendering, matrices, mouseX, mouseY, delta, (List) entries);
+                renderEntries(debugTime, size, time, fastEntryRendering, matrices, mouseX, mouseY, delta, entries);
             }
         }
         
         if (debugTime) {
             long totalTime = System.nanoTime() - totalTimeStart;
-            averageDebugTime += (time[0] / (double) size[0]) * delta;
+            averageDebugTime += (time.getValue() / size.doubleValue()) * delta;
             totalDebugTime += totalTime / 1000000d * delta;
             totalDebugTimeDelta += delta;
             if (totalDebugTimeDelta >= 20) {
@@ -297,12 +308,12 @@ public class EntryListWidget extends WidgetWithBounds {
                 totalDebugTime = 0;
                 totalDebugTimeDelta = 0;
             } else if (lastAverageDebugTime == 0) {
-                lastAverageDebugTime = time[0] / (double) size[0];
+                lastAverageDebugTime = time.getValue() / size.doubleValue();
                 totalDebugTime = totalTime / 1000000d;
             }
             int z = getZ();
             setZ(500);
-            Component debugText = new TextComponent(String.format("%d entries, avg. %.0fns, ttl. %.2fms, %s fps", size[0], lastAverageDebugTime, lastTotalDebugTime, minecraft.fpsString.split(" ")[0]));
+            Component debugText = new TextComponent(String.format("%d entries, avg. %.0fns, ttl. %.2fms, %s fps", size.getValue(), lastAverageDebugTime, lastTotalDebugTime, minecraft.fpsString.split(" ")[0]));
             int stringWidth = font.width(debugText);
             fillGradient(matrices, Math.min(bounds.x, minecraft.screen.width - stringWidth - 2), bounds.y, bounds.x + stringWidth + 2, bounds.y + font.lineHeight + 2, -16777216, -16777216);
             MultiBufferSource.BufferSource immediate = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder());
@@ -374,7 +385,7 @@ public class EntryListWidget extends WidgetWithBounds {
             favoritesListWidget.updateFavoritesBounds(searchTerm);
         if (searchTerm != null)
             updateSearch(searchTerm, true);
-        else if (allStacks == null || (favoritesListWidget != null && favoritesListWidget.favorites == null))
+        else if (allStacks == null || favoritesListWidget == null)
             updateSearch("", true);
         else
             updateEntriesPosition();
@@ -431,7 +442,7 @@ public class EntryListWidget extends WidgetWithBounds {
         }
         FavoritesListWidget favoritesListWidget = ContainerScreenOverlay.getFavoritesListWidget();
         if (favoritesListWidget != null)
-            favoritesListWidget.updateEntriesPosition();
+            favoritesListWidget.updateEntriesPosition(entry -> true);
     }
     
     @ApiStatus.Internal

+ 16 - 12
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/widget/EntryWidget.java

@@ -31,6 +31,7 @@ import me.shedaniel.math.Point;
 import me.shedaniel.math.Rectangle;
 import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.api.*;
+import me.shedaniel.rei.api.favorites.FavoriteEntry;
 import me.shedaniel.rei.api.widgets.Slot;
 import me.shedaniel.rei.api.widgets.Tooltip;
 import me.shedaniel.rei.gui.ContainerScreenOverlay;
@@ -292,11 +293,11 @@ public class EntryWidget extends Slot {
     }
     
     public final boolean hasTooltips() {
-        return tooltips;
+        return isTooltipsEnabled();
     }
     
     public final boolean hasHighlight() {
-        return highlight;
+        return isHighlightEnabled();
     }
     
     protected void drawBackground(PoseStack matrices, int mouseX, int mouseY, float delta) {
@@ -370,13 +371,12 @@ public class EntryWidget extends Slot {
         if (wasClicked() && containsMouse(mouseX, mouseY)) {
             if (interactableFavorites && ConfigObject.getInstance().isFavoritesEnabled() && containsMouse(PointHelper.ofMouse()) && !getCurrentEntry().isEmpty()) {
                 ModifierKeyCode keyCode = ConfigObject.getInstance().getFavoriteKeyCode();
-                EntryStack entry = getCurrentEntry().copy();
-                entry.setAmount(127);
                 if (keyCode.matchesMouse(button)) {
+                    FavoriteEntry favoriteEntry = asFavoriteEntry();
                     if (reverseFavoritesAction())
-                        ConfigObject.getInstance().getFavorites().removeIf(entry::equalsIgnoreAmount);
-                    else if (!CollectionUtils.anyMatchEqualsEntryIgnoreAmount(ConfigObject.getInstance().getFavorites(), entry))
-                        ConfigObject.getInstance().getFavorites().add(entry);
+                        ConfigObject.getInstance().getFavoriteEntries().remove(favoriteEntry);
+                    else if (!ConfigObject.getInstance().getFavoriteEntries().contains(favoriteEntry))
+                        ConfigObject.getInstance().getFavoriteEntries().add(favoriteEntry);
                     ConfigManager.getInstance().saveConfig();
                     FavoritesListWidget favoritesListWidget = ContainerScreenOverlay.getFavoritesListWidget();
                     if (favoritesListWidget != null)
@@ -392,6 +392,11 @@ public class EntryWidget extends Slot {
         return false;
     }
     
+    @ApiStatus.Internal
+    protected FavoriteEntry asFavoriteEntry() {
+        return FavoriteEntry.fromEntryStack(getCurrentEntry().copy());
+    }
+    
     @ApiStatus.Internal
     protected boolean cancelDeleteItems(EntryStack stack) {
         return false;
@@ -408,13 +413,12 @@ public class EntryWidget extends Slot {
         if (containsMouse(PointHelper.ofMouse())) {
             if (interactableFavorites && ConfigObject.getInstance().isFavoritesEnabled() && containsMouse(PointHelper.ofMouse()) && !getCurrentEntry().isEmpty()) {
                 ModifierKeyCode keyCode = ConfigObject.getInstance().getFavoriteKeyCode();
-                EntryStack entry = getCurrentEntry().copy();
-                entry.setAmount(127);
                 if (keyCode.matchesKey(int_1, int_2)) {
+                    FavoriteEntry favoriteEntry = asFavoriteEntry();
                     if (reverseFavoritesAction())
-                        ConfigObject.getInstance().getFavorites().removeIf(entry::equalsIgnoreAmount);
-                    else if (!CollectionUtils.anyMatchEqualsEntryIgnoreAmount(ConfigObject.getInstance().getFavorites(), entry))
-                        ConfigObject.getInstance().getFavorites().add(entry);
+                        ConfigObject.getInstance().getFavoriteEntries().remove(favoriteEntry);
+                    else if (!ConfigObject.getInstance().getFavoriteEntries().contains(favoriteEntry))
+                        ConfigObject.getInstance().getFavoriteEntries().add(favoriteEntry);
                     ConfigManager.getInstance().saveConfig();
                     FavoritesListWidget favoritesListWidget = ContainerScreenOverlay.getFavoritesListWidget();
                     if (favoritesListWidget != null)

+ 781 - 134
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/gui/widget/FavoritesListWidget.java

@@ -23,10 +23,11 @@
 
 package me.shedaniel.rei.gui.widget;
 
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.mojang.blaze3d.vertex.PoseStack;
-import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
-import it.unimi.dsi.fastutil.ints.IntSet;
+import it.unimi.dsi.fastutil.ints.*;
+import it.unimi.dsi.fastutil.objects.ObjectIterator;
 import me.shedaniel.clothconfig2.ClothConfigInitializer;
 import me.shedaniel.clothconfig2.api.ScissorsHandler;
 import me.shedaniel.clothconfig2.api.ScrollingContainer;
@@ -35,23 +36,31 @@ import me.shedaniel.math.Point;
 import me.shedaniel.math.Rectangle;
 import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
-import me.shedaniel.rei.api.*;
-import me.shedaniel.rei.api.widgets.Tooltip;
-import me.shedaniel.rei.gui.config.EntryPanelOrdering;
+import me.shedaniel.rei.api.ConfigManager;
+import me.shedaniel.rei.api.ConfigObject;
+import me.shedaniel.rei.api.DisplayHelper;
+import me.shedaniel.rei.api.REIHelper;
+import me.shedaniel.rei.api.favorites.FavoriteEntry;
+import me.shedaniel.rei.api.favorites.FavoriteMenuEntry;
+import me.shedaniel.rei.gui.ContainerScreenOverlay;
+import me.shedaniel.rei.gui.modules.Menu;
+import me.shedaniel.rei.gui.modules.MenuEntry;
+import me.shedaniel.rei.impl.Animator;
 import me.shedaniel.rei.impl.OptimalEntryStack;
 import me.shedaniel.rei.impl.ScreenHelper;
 import me.shedaniel.rei.utils.CollectionUtils;
 import net.minecraft.client.Minecraft;
-import net.minecraft.client.player.LocalPlayer;
-import net.minecraft.network.chat.TranslatableComponent;
+import net.minecraft.client.gui.components.events.GuiEventListener;
 import net.minecraft.util.Mth;
-import net.minecraft.world.item.Item;
+import net.minecraft.util.Tuple;
 import org.jetbrains.annotations.ApiStatus;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
-import java.util.Collections;
-import java.util.List;
+import java.util.*;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -62,26 +71,31 @@ public class FavoritesListWidget extends WidgetWithBounds {
     protected final ScrollingContainer scrolling = new ScrollingContainer() {
         @Override
         public Rectangle getBounds() {
-            return bounds;
+            return currentBounds;
         }
         
         @Override
         public int getMaxScrollHeight() {
-            return Mth.ceil((favorites.size() + blockedCount) / (innerBounds.width / (float) entrySize())) * entrySize();
+            return Mth.ceil((entries.size() + blockedCount) / (innerBounds.width / (float) entrySize())) * entrySize();
         }
         
         @Override
         public int getScrollBarX() {
             if (!ConfigObject.getInstance().isLeftHandSidePanel())
-                return bounds.x + 1;
-            return bounds.getMaxX() - 7;
+                return fullBounds.x + 1;
+            return fullBounds.getMaxX() - 7;
         }
     };
     protected int blockedCount;
-    List<EntryStack> favorites = null;
-    private Rectangle bounds, innerBounds;
-    private List<EntryListEntry> entries = Collections.emptyList();
-    private boolean draggingScrollBar = false;
+    private Rectangle fullBounds, currentBounds = new Rectangle(), innerBounds;
+    private final Int2ObjectMap<Entry> entries = new Int2ObjectLinkedOpenHashMap<>();
+    private final Int2ObjectMap<Entry> removedEntries = new Int2ObjectLinkedOpenHashMap<>();
+    private List<EntryListEntry> entriesList = Lists.newArrayList();
+    private List<Widget> children = Lists.newArrayList();
+    private Entry lastTouchedEntry = null;
+
+//    public final AddFavoritePanel favoritePanel = new AddFavoritePanel(this);
+//    public final ToggleAddFavoritePanelButton favoritePanelButton = new ToggleAddFavoritePanelButton(this);
     
     private static Rectangle updateInnerBounds(Rectangle bounds) {
         int entrySize = entrySize();
@@ -93,7 +107,10 @@ public class FavoritesListWidget extends WidgetWithBounds {
     
     @Override
     public boolean mouseScrolled(double double_1, double double_2, double double_3) {
-        if (bounds.contains(double_1, double_2)) {
+        if (currentBounds.contains(double_1, double_2)) {
+//            if (favoritePanel.mouseScrolled(double_1, double_2, double_3)) {
+//                return true;
+//            }
             scrolling.offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
             return true;
         }
@@ -103,71 +120,79 @@ public class FavoritesListWidget extends WidgetWithBounds {
     @NotNull
     @Override
     public Rectangle getBounds() {
-        return bounds;
+        return fullBounds;
     }
     
     @Override
     public void render(PoseStack matrices, int mouseX, int mouseY, float delta) {
-        if (bounds.isEmpty())
+        if (fullBounds.isEmpty() || currentBounds.isEmpty())
             return;
+        int entrySize = entrySize();
         boolean fastEntryRendering = ConfigObject.getInstance().doesFastEntryRendering();
-        for (EntryListEntry entry : entries)
-            entry.clearStacks();
-        ScissorsHandler.INSTANCE.scissor(bounds);
-        int skip = Math.max(0, Mth.floor(scrolling.scrollAmount / (float) entrySize()));
-        int nextIndex = skip * innerBounds.width / entrySize();
-        int[] i = {nextIndex};
-        blockedCount = 0;
-        
-        Stream<EntryListEntry> entryStream = this.entries.stream().skip(nextIndex).filter(entry -> {
-            Rectangle entryBounds = entry.getBounds();
+        updateEntriesPosition(entry -> true);
+        if (lastTouchedEntry != null && lastTouchedEntry.dragged) {
+            lastTouchedEntry.x.setAs(mouseX - entrySize / 2);
+            lastTouchedEntry.y.setAs(mouseY - entrySize / 2 + (int) scrolling.scrollAmount);
             
-            entryBounds.y = (int) (entry.backupY - scrolling.scrollAmount);
-            if (entryBounds.y > this.bounds.getMaxY()) return false;
-            if (notSteppingOnExclusionZones(entryBounds.x, entryBounds.y, entryBounds.width, entryBounds.height, innerBounds)) {
-                EntryStack stack = favorites.get(i[0]++);
-                if (!stack.isEmpty()) {
-                    entry.entry(stack);
-                    return true;
-                }
-            } else {
-                blockedCount++;
+            if (!RoughlyEnoughItemsCore.isLeftModePressed)
+                resetDraggedEntry();
+        }
+        for (Entry entry : entries.values()) {
+            entry.update(delta);
+        }
+        if (lastTouchedEntry != null && lastTouchedEntry.madeUp) {
+            lastTouchedEntry.update(delta);
+        }
+        ObjectIterator<Entry> removedEntriesIterator = removedEntries.values().iterator();
+        while (removedEntriesIterator.hasNext()) {
+            Entry removedEntry = removedEntriesIterator.next();
+            removedEntry.update(delta);
+            
+            if (removedEntry.size.doubleValue() <= 300) {
+                removedEntriesIterator.remove();
+                this.entriesList.remove(removedEntry.getWidget());
+                this.children.remove(removedEntry.getWidget());
             }
-            return false;
-        }).limit(Math.max(0, favorites.size() - i[0]));
+        }
+        ScissorsHandler.INSTANCE.scissor(currentBounds);
+        
+        Stream<EntryListEntry> entryStream = this.entriesList.stream()
+                .filter(entry -> lastTouchedEntry == null || entry != lastTouchedEntry.getWidget())
+                .filter(entry -> entry.getBounds().getMaxY() >= this.currentBounds.getY() && entry.getBounds().y <= this.currentBounds.getMaxY());
         
         if (fastEntryRendering) {
-            entryStream.collect(Collectors.groupingBy(entryListEntry -> OptimalEntryStack.groupingHashFrom(entryListEntry.getCurrentEntry()))).forEach((integer, entries) -> {
-                renderEntries(false, new int[]{0}, new long[]{0}, fastEntryRendering, matrices, mouseX, mouseY, delta, (List) entries);
-            });
+            for (List<EntryListEntry> entries : entryStream.collect(Collectors.groupingBy(entryListEntry -> OptimalEntryStack.groupingHashFrom(entryListEntry.getCurrentEntry()))).values()) {
+                renderEntries(true, matrices, mouseX, mouseY, delta, entries);
+            }
         } else {
-            renderEntries(false, new int[]{0}, new long[]{0}, fastEntryRendering, matrices, mouseX, mouseY, delta, entryStream.collect(Collectors.toList()));
+            List<EntryListEntry> widgets = entryStream.collect(Collectors.toList());
+            renderEntries(false, matrices, mouseX, mouseY, delta, widgets);
         }
         
         updatePosition(delta);
         scrolling.renderScrollBar(0, 1, REIHelper.getInstance().isDarkThemeEnabled() ? 0.8f : 1f);
         ScissorsHandler.INSTANCE.removeLastScissor();
-        if (containsMouse(mouseX, mouseY) && ClientHelper.getInstance().isCheating() && !minecraft.player.inventory.getCarried().isEmpty() && RoughlyEnoughItemsCore.canDeleteItems()) {
-            EntryStack stack = EntryStack.create(minecraft.player.inventory.getCarried().copy());
-            if (stack.getType() == EntryStack.Type.FLUID) {
-                Item bucketItem = stack.getFluid().getBucket();
-                if (bucketItem != null) {
-                    stack = EntryStack.create(bucketItem);
-                }
-            }
-            for (Widget child : children()) {
-                if (child.containsMouse(mouseX, mouseY) && child instanceof EntryWidget) {
-                    if (((EntryWidget) child).cancelDeleteItems(stack)) {
-                        return;
-                    }
-                }
-            }
-            Tooltip.create(new TranslatableComponent("text.rei.delete_items")).queue();
+        
+        renderAddFavorite(matrices, mouseX, mouseY, delta);
+        
+        if (lastTouchedEntry != null) {
+            matrices.pushPose();
+            matrices.translate(0, 0, 600);
+            lastTouchedEntry.widget.render(matrices, mouseX, mouseY, delta);
+            matrices.popPose();
         }
     }
     
+    private void renderAddFavorite(PoseStack matrices, int mouseX, int mouseY, float delta) {
+//        this.favoritePanel.render(matrices, mouseX, mouseY, delta);
+//        this.favoritePanelButton.render(matrices, mouseX, mouseY, delta);
+    }
+    
     @Override
     public boolean mouseDragged(double mouseX, double mouseY, int int_1, double double_3, double double_4) {
+        if (lastTouchedEntry != null) {
+            lastTouchedEntry.dragged = true;
+        }
         if (scrolling.mouseDragged(mouseX, mouseY, int_1, double_3, double_4, ConfigObject.getInstance().doesSnapToRows(), entrySize()))
             return true;
         return super.mouseDragged(mouseX, mouseY, int_1, double_3, double_4);
@@ -194,123 +219,745 @@ public class FavoritesListWidget extends WidgetWithBounds {
     }
     
     public void updateFavoritesBounds(@Nullable String searchTerm) {
-        this.bounds = ScreenHelper.getFavoritesListArea(DisplayHelper.getInstance().getOverlayBounds(ConfigObject.getInstance().getDisplayPanelLocation().mirror(), Minecraft.getInstance().screen));
+        this.fullBounds = ScreenHelper.getFavoritesListArea(DisplayHelper.getInstance().getOverlayBounds(ConfigObject.getInstance().getDisplayPanelLocation().mirror(), Minecraft.getInstance().screen));
     }
     
     public void updateSearch(EntryListWidget listWidget, String searchTerm) {
         if (ConfigObject.getInstance().isFavoritesEnabled()) {
-            if (ConfigObject.getInstance().doSearchFavorites()) {
-                List<EntryStack> list = Lists.newArrayList();
-                boolean checkCraftable = ConfigManager.getInstance().isCraftableOnlyEnabled() && !ScreenHelper.inventoryStacks.isEmpty();
-                IntSet workingItems = checkCraftable ? new IntOpenHashSet() : null;
-                if (checkCraftable)
-                    workingItems.addAll(CollectionUtils.map(RecipeHelper.getInstance().findCraftableEntriesByItems(ScreenHelper.inventoryStacks), EntryStack::hashIgnoreAmount));
-                for (EntryStack stack : ConfigObject.getInstance().getFavorites()) {
-                    if (listWidget.canLastSearchTermsBeAppliedTo(stack)) {
-                        if (checkCraftable && !workingItems.contains(stack.hashIgnoreAmount()))
-                            continue;
-                        list.add(stack.copy().setting(EntryStack.Settings.RENDER_COUNTS, EntryStack.Settings.FALSE));
-                    }
-                }
-                EntryPanelOrdering ordering = ConfigObject.getInstance().getItemListOrdering();
-                if (ordering == EntryPanelOrdering.NAME)
-                    list.sort(ENTRY_NAME_COMPARER);
-                if (ordering == EntryPanelOrdering.GROUPS)
-                    list.sort(ENTRY_GROUP_COMPARER);
-                if (!ConfigObject.getInstance().isItemListAscending())
-                    Collections.reverse(list);
-                favorites = list;
-            } else {
-                List<EntryStack> list = Lists.newArrayList();
-                boolean checkCraftable = ConfigManager.getInstance().isCraftableOnlyEnabled() && !ScreenHelper.inventoryStacks.isEmpty();
-                IntSet workingItems = checkCraftable ? new IntOpenHashSet() : null;
-                if (checkCraftable)
-                    workingItems.addAll(CollectionUtils.map(RecipeHelper.getInstance().findCraftableEntriesByItems(ScreenHelper.inventoryStacks), EntryStack::hashIgnoreAmount));
-                for (EntryStack stack : ConfigObject.getInstance().getFavorites()) {
-                    if (checkCraftable && !workingItems.contains(stack.hashIgnoreAmount()))
-                        continue;
-                    list.add(stack.copy().setting(EntryStack.Settings.RENDER_COUNTS, EntryStack.Settings.FALSE));
-                }
-                EntryPanelOrdering ordering = ConfigObject.getInstance().getItemListOrdering();
-                if (ordering == EntryPanelOrdering.NAME)
-                    list.sort(ENTRY_NAME_COMPARER);
-                if (ordering == EntryPanelOrdering.GROUPS)
-                    list.sort(ENTRY_GROUP_COMPARER);
-                if (!ConfigObject.getInstance().isItemListAscending())
-                    Collections.reverse(list);
-                favorites = list;
+            applyNewFavorites(CollectionUtils.map(ConfigObject.getInstance().getFavoriteEntries(), FavoriteEntry::copy));
+        } else applyNewFavorites(Collections.emptyList());
+    }
+    
+    public void applyNewFavorites(List<FavoriteEntry> newFavorites) {
+        newFavorites = Lists.newArrayList(newFavorites);
+        newFavorites.removeIf(FavoriteEntry::isEntryInvalid);
+        
+        int entrySize = entrySize();
+        IntSet newFavoritesHash = new IntOpenHashSet(CollectionUtils.mapToInt(newFavorites, FavoriteEntry::hashIgnoreAmount));
+        List<Entry> removedEntries = Lists.newArrayList(this.entries.values());
+        removedEntries.removeIf(entry -> newFavoritesHash.contains(entry.hashIgnoreAmount()));
+        
+        for (Entry removedEntry : removedEntries) {
+            removedEntry.remove();
+            this.removedEntries.put(removedEntry.hashIgnoreAmount(), removedEntry);
+        }
+        
+        Int2ObjectMap<Entry> prevEntries = new Int2ObjectOpenHashMap<>(entries);
+        this.entries.clear();
+        
+        for (FavoriteEntry favorite : newFavorites) {
+            Entry entry = prevEntries.get(favorite.hashIgnoreAmount());
+            
+            if (entry == null) {
+                entry = new Entry(favorite, entrySize);
             }
-        } else
-            favorites = Collections.emptyList();
+            
+            if (ConfigObject.getInstance().isReducedMotion()) entry.size.setAs(entrySize * 100);
+            else entry.size.setTo(entrySize * 100, 300);
+            entries.put(entry.hashIgnoreAmount(), entry);
+        }
+        
+        applyNewEntriesList();
+        updateEntriesPosition(entry -> prevEntries.containsKey(entry.hashIgnoreAmount()));
+    }
+    
+    public void applyNewEntriesList() {
+        this.entriesList = Stream.concat(entries.values().stream().map(Entry::getWidget), removedEntries.values().stream().map(Entry::getWidget)).collect(Collectors.toList());
+        this.children = Stream.<Stream<Widget>>of(
+                entries.values().stream().map(Entry::getWidget),
+                removedEntries.values().stream().map(Entry::getWidget)
+//                Stream.of(favoritePanelButton, favoritePanel)
+        ).flatMap(Function.identity()).collect(Collectors.toList());
     }
     
-    public void updateEntriesPosition() {
+    public void updateEntriesPosition(Predicate<Entry> animated) {
         int entrySize = entrySize();
-        this.innerBounds = updateInnerBounds(bounds);
+        this.blockedCount = 0;
+//        if (favoritePanel.getBounds().height > 20)
+//            this.currentBounds.setBounds(this.fullBounds.x, this.fullBounds.y, this.fullBounds.width, this.fullBounds.height - (this.fullBounds.getMaxY() - this.favoritePanel.bounds.y) - 4);
+//        else this.currentBounds.setBounds(this.fullBounds);
+        this.currentBounds.setBounds(this.fullBounds);
+        this.innerBounds = updateInnerBounds(currentBounds);
         int width = innerBounds.width / entrySize;
-        int pageHeight = innerBounds.height / entrySize;
-        int slotsToPrepare = Math.max(favorites.size() * 3, width * pageHeight * 3);
         int currentX = 0;
         int currentY = 0;
-        List<EntryListEntry> entries = Lists.newArrayList();
-        for (int i = 0; i < slotsToPrepare; i++) {
-            int xPos = currentX * entrySize + innerBounds.x;
-            int yPos = currentY * entrySize + innerBounds.y;
-            entries.add((EntryListEntry) new EntryListEntry(xPos, yPos, entrySize).noBackground());
-            currentX++;
-            if (currentX >= width) {
-                currentX = 0;
-                currentY++;
+        int releaseIndex = lastTouchedEntry != null && lastTouchedEntry.dragged ? getReleaseIndex() : -2;
+        
+        int slotIndex = 0;
+        for (Entry entry : this.entries.values()) {
+            if (entry.dragged) continue;
+            while (true) {
+                int xPos = currentX * entrySize + innerBounds.x;
+                int yPos = currentY * entrySize + innerBounds.y;
+                
+                currentX++;
+                if (currentX >= width) {
+                    currentX = 0;
+                    currentY++;
+                }
+                
+                if (notSteppingOnExclusionZones(xPos, yPos - (int) scrolling.scrollAmount, entrySize, entrySize, innerBounds)) {
+                    if (slotIndex++ == releaseIndex) {
+                        continue;
+                    }
+                    
+                    entry.moveTo(animated.test(entry), xPos, yPos);
+                    break;
+                } else {
+                    blockedCount++;
+                }
             }
         }
-        this.entries = entries;
     }
     
     @Override
     public List<? extends Widget> children() {
-        return entries;
+        return children;
+    }
+    
+    public int getReleaseIndex() {
+        if (lastTouchedEntry != null && lastTouchedEntry.dragged && currentBounds.contains(PointHelper.ofMouse())) {
+            int entrySize = entrySize();
+            int width = innerBounds.width / entrySize;
+            int currentX = 0;
+            int currentY = 0;
+            List<Tuple<Entry, Point>> entriesPoints = Lists.newArrayList();
+            for (Entry entry : this.entries.values()) {
+                if (entry.dragged) continue;
+                while (true) {
+                    int xPos = currentX * entrySize + innerBounds.x;
+                    int yPos = currentY * entrySize + innerBounds.y;
+                    
+                    currentX++;
+                    if (currentX >= width) {
+                        currentX = 0;
+                        currentY++;
+                    }
+                    
+                    if (notSteppingOnExclusionZones(xPos, yPos - (int) scrolling.scrollAmount, entrySize, entrySize, innerBounds)) {
+                        entriesPoints.add(new Tuple<>(entry, new Point(xPos, yPos)));
+                        break;
+                    } else {
+                        blockedCount++;
+                    }
+                }
+            }
+            
+            int maxSize = entriesPoints.size();
+            if (currentX != 0) {
+                int xPos = currentX * entrySize + innerBounds.x;
+                int yPos = currentY * entrySize + innerBounds.y;
+                
+                if (notSteppingOnExclusionZones(xPos, yPos - (int) scrolling.scrollAmount, entrySize, entrySize, innerBounds)) {
+                    entriesPoints.add(new Tuple<>(null, new Point(xPos, yPos)));
+                }
+            }
+            
+            double x = lastTouchedEntry.x.doubleValue();
+            double y = lastTouchedEntry.y.doubleValue();
+            
+            return Mth.clamp(entriesPoints.stream()
+                            .filter(value -> {
+                                double otherY = value.getB().y;
+                                
+                                return otherY <= y + entrySize / 2 && otherY + entrySize > y + entrySize / 2;
+                            })
+                            .min(Comparator.comparingDouble(value -> {
+                                double otherX = value.getB().x;
+                                double otherY = value.getB().y;
+                                
+                                return (x - otherX) * (x - otherX) + (y - otherY) * (y - otherY);
+                            }))
+                            .map(entriesPoints::indexOf)
+                            .orElse(maxSize),
+                    0, entriesPoints.size());
+        }
+        
+        return entries.size();
+    }
+    
+    public void resetDraggedEntry() {
+        if (lastTouchedEntry != null && lastTouchedEntry.dragged) {
+            Entry entry = lastTouchedEntry;
+            double x = entry.x.doubleValue();
+            double y = entry.y.doubleValue();
+            
+            boolean contains = currentBounds.contains(PointHelper.ofMouse());
+            if (contains || !entry.madeUp) {
+                int newIndex = contains ? getReleaseIndex() : Iterables.indexOf(entries.values(), e -> e == entry);
+                entry.dragged = false;
+                
+                if (entries.size() - 1 <= newIndex) {
+                    this.entries.remove(entry.hashIgnoreAmount());
+                    this.entries.put(entry.hashIgnoreAmount(), entry);
+                } else {
+                    Int2ObjectMap<Entry> prevEntries = new Int2ObjectLinkedOpenHashMap<>(entries);
+                    this.entries.clear();
+                    
+                    int index = 0;
+                    for (Int2ObjectMap.Entry<Entry> entryEntry : prevEntries.int2ObjectEntrySet()) {
+                        if (index == newIndex)
+                            this.entries.put(entry.hashIgnoreAmount(), entry);
+                        if (entryEntry.getIntKey() != entry.hashIgnoreAmount()) {
+                            this.entries.put(entryEntry.getIntKey(), entryEntry.getValue());
+                            
+                            index++;
+                        }
+                    }
+                }
+                
+                applyNewEntriesList();
+                
+                if (ConfigObject.getInstance().isFavoritesEnabled()) {
+                    List<FavoriteEntry> favorites = ConfigObject.getInstance().getFavoriteEntries();
+                    favorites.clear();
+                    for (Entry value : this.entries.values()) {
+                        favorites.add(value.entry.copy());
+                    }
+                    
+                    ConfigManager.getInstance().saveConfig();
+                }
+                
+                if (entry.madeUp) {
+                    applyNewFavorites(this.entries.values().stream()
+                            .map(Entry::getEntry)
+                            .collect(Collectors.toList()));
+                }
+                
+                entry.madeUp = false;
+            }
+        }
+        
+        lastTouchedEntry = null;
     }
     
     @Override
-    public boolean mouseClicked(double double_1, double double_2, int int_1) {
-        if (scrolling.updateDraggingState(double_1, double_2, int_1))
+    public boolean mouseClicked(double mouseX, double mouseY, int int_1) {
+        resetDraggedEntry();
+        
+        if (scrolling.updateDraggingState(mouseX, mouseY, int_1))
             return true;
+        if (innerBounds.contains(mouseX, mouseY)) {
+            for (Entry entry : entries.values()) {
+                if (entry.getWidget().containsMouse(mouseX, mouseY)) {
+                    lastTouchedEntry = entry;
+                    break;
+                }
+            }
+        } /*else if (favoritePanel.bounds.contains(mouseX, mouseY)) {
+            back:
+            for (AddFavoritePanel.Row row : favoritePanel.rows.get()) {
+                if (row instanceof AddFavoritePanel.SectionEntriesRow) {
+                    for (AddFavoritePanel.SectionEntriesRow.SectionFavoriteWidget widget : ((AddFavoritePanel.SectionEntriesRow) row).widgets) {
+                        if (widget.containsMouse(mouseX, mouseY)) {
+                            lastTouchedEntry = new Entry(widget.entry.copy(), entrySize());
+                            lastTouchedEntry.madeUp = true;
+                            lastTouchedEntry.dragged = true;
+                            lastTouchedEntry.size.setAs(entrySize() * 100);
+                            applyNewFavorites(this.entries.values().stream()
+                                    .map(Entry::getEntry)
+                                    .map(FavoriteEntry::copy)
+                                    .filter(entry -> !entry.equals(widget.entry))
+                                    .collect(Collectors.toList()));
+                            break back;
+                        }
+                    }
+                }
+            }
+        }*/
         for (Widget widget : children())
-            if (widget.mouseClicked(double_1, double_2, int_1))
+            if (widget.mouseClicked(mouseX, mouseY, int_1))
                 return true;
-        return false;
+        return lastTouchedEntry != null;
     }
     
     @Override
     public boolean mouseReleased(double mouseX, double mouseY, int button) {
+        if (lastTouchedEntry != null && lastTouchedEntry.dragged) {
+            resetDraggedEntry();
+            return true;
+        }
+        
+        lastTouchedEntry = null;
+        
         if (containsMouse(mouseX, mouseY)) {
             for (Widget widget : children())
                 if (widget.mouseReleased(mouseX, mouseY, button))
                     return true;
-            LocalPlayer player = minecraft.player;
-            if (ClientHelper.getInstance().isCheating() && player != null && player.inventory != null && !player.inventory.getCarried().isEmpty() && RoughlyEnoughItemsCore.canDeleteItems()) {
-                ClientHelper.getInstance().sendDeletePacket();
-                return true;
-            }
-            if (player != null && player.inventory != null && !player.inventory.getCarried().isEmpty() && RoughlyEnoughItemsCore.hasPermissionToUsePackets())
-                return false;
         }
         return false;
     }
     
+    public class Entry {
+        private FavoriteEntry entry;
+        private final EntryListEntry widget;
+        private boolean hidden;
+        private boolean dragged;
+        private boolean madeUp;
+        private Animator x = new Animator();
+        private Animator y = new Animator();
+        private Animator size = new Animator();
+        
+        public Entry(FavoriteEntry entry, int entrySize) {
+            this.entry = entry;
+            this.widget = (EntryListEntry) new EntryListEntry(this, 0, 0, entrySize, entry).noBackground();
+        }
+        
+        public void remove() {
+            if (!hidden) {
+                this.hidden = true;
+                if (ConfigObject.getInstance().isReducedMotion()) this.size.setAs(0);
+                else this.size.setTo(0, 300);
+            }
+        }
+        
+        public void update(double delta) {
+            this.size.update(delta);
+            this.x.update(delta);
+            this.y.update(delta);
+            this.getWidget().getBounds().width = this.getWidget().getBounds().height = (int) Math.round(this.size.doubleValue() / 100);
+            double offsetSize = (entrySize() - this.size.doubleValue() / 100) / 2;
+            this.getWidget().getBounds().x = (int) Math.round(x.doubleValue() + offsetSize);
+            this.getWidget().getBounds().y = (int) Math.round(y.doubleValue() + offsetSize) - (int) scrolling.scrollAmount;
+        }
+        
+        public EntryListEntry getWidget() {
+            return widget;
+        }
+        
+        public boolean isHidden() {
+            return hidden;
+        }
+        
+        public int hashIgnoreAmount() {
+            return entry.hashIgnoreAmount();
+        }
+        
+        public FavoriteEntry getEntry() {
+            return entry;
+        }
+        
+        public void moveTo(boolean animated, int xPos, int yPos) {
+            if (animated && !ConfigObject.getInstance().isReducedMotion()) {
+                x.setTo(xPos, 200);
+                y.setTo(yPos, 200);
+            } else {
+                x.setAs(xPos);
+                y.setAs(yPos);
+            }
+        }
+    }
+    
     private class EntryListEntry extends EntryListEntryWidget {
-        private EntryListEntry(int x, int y, int entrySize) {
+        private final Entry entry;
+        private final FavoriteEntry favoriteEntry;
+        
+        private EntryListEntry(Entry entry, int x, int y, int entrySize, FavoriteEntry favoriteEntry) {
             super(new Point(x, y), entrySize);
+            this.entry = entry;
+            this.favoriteEntry = favoriteEntry;
+            this.clearEntries().entry(this.favoriteEntry.getWidget(false));
+        }
+        
+        @Override
+        protected FavoriteEntry asFavoriteEntry() {
+            return favoriteEntry.copy();
         }
         
         @Override
         public boolean containsMouse(double mouseX, double mouseY) {
-            return super.containsMouse(mouseX, mouseY) && bounds.contains(mouseX, mouseY);
+            return super.containsMouse(mouseX, mouseY) && currentBounds.contains(mouseX, mouseY);
         }
         
         @Override
         protected boolean reverseFavoritesAction() {
             return true;
         }
+        
+        @Override
+        public void queueTooltip(PoseStack matrices, int mouseX, int mouseY, float delta) {
+            if (lastTouchedEntry == null || !lastTouchedEntry.dragged)
+                super.queueTooltip(matrices, mouseX, mouseY, delta);
+        }
+        
+        @Override
+        protected void drawHighlighted(PoseStack matrices, int mouseX, int mouseY, float delta) {
+            if (lastTouchedEntry == null || !lastTouchedEntry.dragged)
+                super.drawHighlighted(matrices, mouseX, mouseY, delta);
+        }
+        
+        @Override
+        public void render(PoseStack matrices, int mouseX, int mouseY, float delta) {
+            Optional<ContainerScreenOverlay> overlayOptional = ScreenHelper.getOptionalOverlay();
+            Optional<Supplier<Collection<@NotNull FavoriteMenuEntry>>> menuEntries = favoriteEntry.getMenuEntries();
+            if (Math.abs(entry.x.doubleValue() - entry.x.target()) < 1 && Math.abs(entry.y.doubleValue() - entry.y.target()) < 1 && overlayOptional.isPresent() && menuEntries.isPresent()) {
+                ContainerScreenOverlay overlay = overlayOptional.get();
+                UUID uuid = favoriteEntry.getUuid();
+                
+                boolean isOpened = overlay.isMenuOpened(uuid);
+                if (entry.dragged || entry.madeUp) {
+                    if (isOpened) {
+                        overlay.removeOverlayMenu();
+                    }
+                } else if (isOpened || !overlay.isAnyMenuOpened()) {
+                    boolean inBounds = containsMouse(mouseX, mouseY) || overlay.isMenuInBounds(uuid);
+                    if (isOpened != inBounds) {
+                        if (inBounds) {
+                            Menu menu = new Menu(new Point(getBounds().x, getBounds().getMaxY()),
+                                    CollectionUtils.map(menuEntries.get().get(), entry -> new MenuEntry() {
+                                        @Override
+                                        public List<? extends GuiEventListener> children() {
+                                            return Collections.singletonList(entry);
+                                        }
+                                        
+                                        @Override
+                                        public void render(PoseStack poseStack, int i, int j, float f) {
+                                            entry.render(poseStack, i, j, f);
+                                        }
+                                        
+                                        @Override
+                                        public int getEntryWidth() {
+                                            return entry.getEntryWidth();
+                                        }
+                                        
+                                        @Override
+                                        public int getEntryHeight() {
+                                            return entry.getEntryHeight();
+                                        }
+                                        
+                                        @Override
+                                        public void updateInformation(int xPos, int yPos, boolean selected, boolean containsMouse, boolean rendering, int width) {
+                                            entry.closeMenu = overlay::removeOverlayMenu;
+                                            entry.updateInformation(xPos, yPos, selected, containsMouse, rendering, width);
+                                        }
+                                        
+                                        @Override
+                                        public int getZ() {
+                                            return entry.getZ();
+                                        }
+                                        
+                                        @Override
+                                        public void setZ(int z) {
+                                            entry.setZ(z);
+                                        }
+                                    }));
+                            if (ConfigObject.getInstance().isLeftHandSidePanel())
+                                menu.menuStartPoint.x -= menu.getBounds().width - getBounds().width;
+                            overlay.openMenu(uuid, menu, this::containsMouse);
+                        } else {
+                            overlay.removeOverlayMenu();
+                        }
+                    }
+                }
+            }
+            super.render(matrices, mouseX, mouseY, delta);
+        }
+        
+        @Override
+        protected boolean doAction(double mouseX, double mouseY, int button) {
+            return favoriteEntry.doAction(button);
+        }
+    }
+    
+    /*public static class ToggleAddFavoritePanelButton extends WidgetWithBounds {
+        private final FavoritesListWidget widget;
+        public boolean wasClicked = false;
+        public final Animator alpha = new Animator(0);
+        
+        public final Rectangle bounds = new Rectangle();
+        
+        public ToggleAddFavoritePanelButton(FavoritesListWidget widget) {
+            this.widget = widget;
+        }
+        
+        @Override
+        public void render(PoseStack matrices, int mouseX, int mouseY, float delta) {
+            float expendProgress = widget.favoritePanel.expendState.floatValue();
+            this.bounds.setBounds(updateAddFavoriteButtonArea(widget.fullBounds));
+            
+            boolean isHoveringAddFavoriteButton = containsMouse(mouseX, mouseY);
+            this.alpha.setTo(isHoveringAddFavoriteButton ? 1f : widget.fullBounds.contains(mouseX, mouseY) || expendProgress > .1f ? 0.3f : 0f, 260);
+            this.alpha.update(delta);
+            int buttonColor = 0xFFFFFF | (Math.round(0x34 * alpha.floatValue()) << 24);
+            fillGradient(matrices, bounds.x, bounds.y, bounds.getMaxX(), bounds.getMaxY(), buttonColor, buttonColor);
+            if (isVisible()) {
+                MultiBufferSource.BufferSource bufferSource = MultiBufferSource.immediate(Tesselator.getInstance().getBuilder());
+                if (expendProgress < .9f) {
+                    int textColor = 0xFFFFFF | (Math.round(0xef * alpha.floatValue() * (1 - expendProgress)) << 24);
+                    font.drawInBatch("+", bounds.getCenterX() - 2.5f, bounds.getCenterY() - 3, textColor, false, matrices.last().pose(), bufferSource, false, 0, 15728880);
+                }
+                if (expendProgress > .1f) {
+                    int textColor = 0xFFFFFF | (Math.round(0xef * alpha.floatValue() * expendProgress) << 24);
+                    font.drawInBatch("-", bounds.getCenterX() - 2.5f, bounds.getCenterY() - 3, textColor, false, matrices.last().pose(), bufferSource, false, 0, 15728880);
+                }
+                bufferSource.endBatch();
+            }
+            if (isHoveringAddFavoriteButton) {
+                Tooltip.create(new TranslatableComponent("text.rei.add_favorite_widget")).queue();
+            }
+        }
+        
+        @NotNull
+        @Override
+        public Rectangle getBounds() {
+            return bounds;
+        }
+        
+        public boolean isVisible() {
+            return Math.round(0x12 * alpha.floatValue()) > 0;
+        }
+        
+        private Rectangle updateAddFavoriteButtonArea(Rectangle fullArea) {
+            return new Rectangle(fullArea.x + 4, fullArea.getMaxY() - 16 - 4, 16, 16);
+        }
+        
+        protected boolean wasClicked() {
+            boolean b = this.wasClicked;
+            this.wasClicked = false;
+            return b;
+        }
+        
+        @Override
+        public List<? extends GuiEventListener> children() {
+            return Collections.emptyList();
+        }
+        
+        @Override
+        public boolean mouseClicked(double mouseX, double mouseY, int button) {
+            if (isVisible() && containsMouse(mouseX, mouseY))
+                this.wasClicked = true;
+            return false;
+        }
+        
+        @Override
+        public boolean mouseReleased(double mouseX, double mouseY, int button) {
+            if (wasClicked() && isVisible() && containsMouse(mouseX, mouseY)) {
+                widget.favoritePanel.expendState.setTo(widget.favoritePanel.expendState.target() == 1 ? 0 : 1, 1500);
+                widget.favoritePanel.resetRows();
+                return true;
+            }
+            return false;
+        }
     }
+    
+    public static class AddFavoritePanel extends WidgetWithBounds {
+        private final FavoritesListWidget widget;
+        public final Animator expendState = new Animator(0);
+        private final Rectangle bounds = new Rectangle();
+        private final Rectangle scrollBounds = new Rectangle();
+        private final LazyResettable<List<Row>> rows = new LazyResettable<>(() -> {
+            List<Row> rows = new ArrayList<>();
+            for (FavoriteEntryType.Section section : FavoriteEntryType.registry().sections()) {
+                rows.add(new SectionRow(section.getText().copy().withStyle(style -> style.withBold(true))));
+                rows.add(new SectionEntriesRow(CollectionUtils.map(section.getEntries(), FavoriteEntry::copy)));
+            }
+            return rows;
+        });
+        private final ScrollingContainer scroller = new ScrollingContainer() {
+            @Override
+            public Rectangle getBounds() {
+                return scrollBounds;
+            }
+            
+            @Override
+            public int getMaxScrollHeight() {
+                return rows.get().stream().mapToInt(Row::getRowHeight).sum();
+            }
+        };
+        
+        public AddFavoritePanel(FavoritesListWidget widget) {
+            this.widget = widget;
+        }
+        
+        public void resetRows() {
+            this.rows.reset();
+        }
+        
+        @Override
+        public void render(PoseStack matrices, int mouseX, int mouseY, float delta) {
+            this.bounds.setBounds(updatePanelArea(widget.fullBounds));
+            this.scrollBounds.setBounds(bounds.x + 4, bounds.y + 4, bounds.width - 8, bounds.height - 20);
+            this.expendState.update(delta);
+            int buttonColor = 0xFFFFFF | (Math.round(0x34 * Math.min(expendState.floatValue() * 2, 1)) << 24);
+            fillGradient(matrices, bounds.x, bounds.y, bounds.getMaxX(), bounds.getMaxY(), buttonColor, buttonColor);
+            scroller.updatePosition(delta);
+            
+            if (expendState.floatValue() > 0.1f) {
+                ScissorsHandler.INSTANCE.scissor(scrollBounds);
+                int y = scrollBounds.y - (int) scroller.scrollAmount;
+                for (Row row : rows.get()) {
+                    row.render(matrices, scrollBounds.x, y, scrollBounds.width, row.getRowHeight(), mouseX, mouseY, delta);
+                    y += row.getRowHeight();
+                }
+                scroller.renderScrollBar(0, 1, 1f);
+                ScissorsHandler.INSTANCE.removeLastScissor();
+            }
+        }
+        
+        private Rectangle updatePanelArea(Rectangle fullArea) {
+            int currentWidth = 16 + Math.round(Math.min(expendState.floatValue(), 1) * (fullArea.getWidth() - 16 - 12));
+            int currentHeight = 16 + Math.round(expendState.floatValue() * (fullArea.getHeight() * 0.4f - 16 - 8));
+            return new Rectangle(fullArea.x + 4, fullArea.getMaxY() - currentHeight - 4, currentWidth, currentHeight);
+        }
+        
+        @Override
+        public boolean mouseScrolled(double d, double e, double f) {
+            if (scrollBounds.contains(d, e)) {
+                scroller.offset(ClothConfigInitializer.getScrollStep() * -f, true);
+                return true;
+            }
+            return super.mouseScrolled(d, e, f);
+        }
+        
+        @Override
+        public List<? extends GuiEventListener> children() {
+            return rows.get();
+        }
+        
+        @NotNull
+        @Override
+        public Rectangle getBounds() {
+            return bounds;
+        }
+        
+        private static abstract class Row extends AbstractContainerEventHandler {
+            public abstract int getRowHeight();
+            
+            public abstract void render(PoseStack matrices, int x, int y, int rowWidth, int rowHeight, int mouseX, int mouseY, float delta);
+        }
+        
+        private static class SectionRow extends Row {
+            private final Component text;
+            
+            public SectionRow(Component text) {
+                this.text = text;
+            }
+            
+            @Override
+            public int getRowHeight() {
+                return 11;
+            }
+            
+            @Override
+            public void render(PoseStack matrices, int x, int y, int rowWidth, int rowHeight, int mouseX, int mouseY, float delta) {
+                Minecraft.getInstance().font.draw(matrices, text, x, y + 1, 0xFFFFFFFF);
+            }
+            
+            @Override
+            public List<? extends GuiEventListener> children() {
+                return Collections.emptyList();
+            }
+        }
+        
+        private class SectionEntriesRow extends Row {
+            private final List<FavoriteEntry> entries;
+            private final List<SectionFavoriteWidget> widgets;
+            private int blockedCount;
+            private int lastY;
+            
+            public SectionEntriesRow(List<FavoriteEntry> entries) {
+                this.entries = entries;
+                int entrySize = entrySize();
+                this.widgets = CollectionUtils.map(this.entries, entry -> new SectionFavoriteWidget(new Point(0, 0), entrySize, entry));
+                
+                for (SectionFavoriteWidget widget : this.widgets) {
+                    widget.size.setTo(entrySize * 100, 300);
+                }
+                
+                this.lastY = scrollBounds.y;
+                
+                updateEntriesPosition(widget -> false);
+            }
+            
+            @Override
+            public int getRowHeight() {
+                return Mth.ceil((entries.size() + blockedCount) / (scrollBounds.width / (float) entrySize())) * entrySize();
+            }
+            
+            @Override
+            public void render(PoseStack matrices, int x, int y, int rowWidth, int rowHeight, int mouseX, int mouseY, float delta) {
+                this.lastY = y;
+                int entrySize = entrySize();
+                boolean fastEntryRendering = ConfigObject.getInstance().doesFastEntryRendering();
+                updateEntriesPosition(entry -> true);
+                for (SectionFavoriteWidget widget : widgets) {
+                    widget.update(delta);
+                    
+                    if (widget.getBounds().getMaxY() > lastY && widget.getBounds().getY() <= lastY + rowHeight) {
+                        if (widget.getCurrentEntry().isEmpty())
+                            continue;
+                        widget.render(matrices, mouseX, mouseY, delta);
+                    }
+                }
+            }
+            
+            @Override
+            public List<? extends GuiEventListener> children() {
+                return widgets;
+            }
+            
+            private class SectionFavoriteWidget extends EntryListEntryWidget {
+                private Animator x = new Animator();
+                private Animator y = new Animator();
+                private Animator size = new Animator();
+                private FavoriteEntry entry;
+                
+                protected SectionFavoriteWidget(Point point, int entrySize, FavoriteEntry entry) {
+                    super(point, entrySize);
+                    this.entry = entry;
+                    entry(entry.getWidget(true));
+                    noBackground();
+                }
+                
+                public void moveTo(boolean animated, int xPos, int yPos) {
+                    if (animated) {
+                        x.setTo(xPos, 200);
+                        y.setTo(yPos, 200);
+                    } else {
+                        x.setAs(xPos);
+                        y.setAs(yPos);
+                    }
+                }
+                
+                public void update(float delta) {
+                    this.size.update(delta);
+                    this.x.update(delta);
+                    this.y.update(delta);
+                    this.getBounds().width = this.getBounds().height = (int) Math.round(this.size.doubleValue() / 100);
+                    double offsetSize = (entrySize() - this.size.doubleValue() / 100) / 2;
+                    this.getBounds().x = (int) Math.round(x.doubleValue() + offsetSize);
+                    this.getBounds().y = (int) Math.round(y.doubleValue() + offsetSize) - (int) scroller.scrollAmount;
+                }
+            }
+            
+            public void updateEntriesPosition(Predicate<SectionFavoriteWidget> animated) {
+                int entrySize = entrySize();
+                this.blockedCount = 0;
+                int width = scrollBounds.width / entrySize;
+                int currentX = 0;
+                int currentY = 0;
+                
+                int slotIndex = 0;
+                for (SectionFavoriteWidget widget : this.widgets) {
+                    while (true) {
+                        int xPos = currentX * entrySize + scrollBounds.x;
+                        int yPos = currentY * entrySize + lastY + (int) scroller.scrollAmount;
+                        
+                        currentX++;
+                        if (currentX >= width) {
+                            currentX = 0;
+                            currentY++;
+                        }
+                        
+                        if (notSteppingOnExclusionZones(xPos, yPos, entrySize, entrySize, scrollBounds)) {
+                            widget.moveTo(animated.test(widget), xPos, yPos);
+                            break;
+                        } else {
+                            blockedCount++;
+                        }
+                    }
+                }
+            }
+        }
+    }*/
 }

+ 97 - 0
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/Animator.java

@@ -0,0 +1,97 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl;
+
+import me.shedaniel.clothconfig2.api.ScrollingContainer;
+import net.minecraft.Util;
+import org.jetbrains.annotations.ApiStatus;
+
+@ApiStatus.Internal
+public final class Animator extends Number {
+    private double amount;
+    private double target;
+    private long start;
+    private long duration;
+    
+    public Animator() {
+    }
+    
+    public Animator(double amount) {
+        setAs(amount);
+    }
+    
+    public void setAs(double value) {
+        this.set(value, false, 0);
+    }
+    
+    public void setTo(double value, long duration) {
+        if (target != value)
+            this.set(value, true, duration);
+    }
+    
+    private void set(double value, boolean animated, long duration) {
+        this.target = value;
+        this.start = Util.getMillis();
+        
+        if (animated) {
+            this.duration = duration;
+        } else {
+            this.duration = 0;
+            this.amount = this.target;
+        }
+    }
+    
+    public void update(double delta) {
+        if (duration != 0) {
+            if (amount < target)
+                this.amount = Math.min(ScrollingContainer.ease(amount, target + (target - amount), Math.min(((double) Util.getMillis() - start) / duration * delta * 3.0D, 1.0D), v -> v), target);
+            else if (amount > target)
+                this.amount = Math.max(ScrollingContainer.ease(amount, target - (amount - target), Math.min(((double) Util.getMillis() - start) / duration * delta * 3.0D, 1.0D), v -> v), target);
+        }
+    }
+    
+    @Override
+    public int intValue() {
+        return (int) amount;
+    }
+    
+    @Override
+    public long longValue() {
+        return (long) amount;
+    }
+    
+    @Override
+    public float floatValue() {
+        return (float) amount;
+    }
+    
+    @Override
+    public double doubleValue() {
+        return amount;
+    }
+    
+    public double target() {
+        return target;
+    }
+}

+ 28 - 4
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/ConfigManagerImpl.java

@@ -36,8 +36,10 @@ import me.sargunvohra.mcmods.autoconfig1u.gui.ConfigScreenProvider;
 import me.sargunvohra.mcmods.autoconfig1u.gui.registry.GuiRegistry;
 import me.sargunvohra.mcmods.autoconfig1u.serializer.JanksonConfigSerializer;
 import me.sargunvohra.mcmods.autoconfig1u.shadowed.blue.endless.jankson.Jankson;
+import me.sargunvohra.mcmods.autoconfig1u.shadowed.blue.endless.jankson.JsonNull;
 import me.sargunvohra.mcmods.autoconfig1u.shadowed.blue.endless.jankson.JsonObject;
 import me.sargunvohra.mcmods.autoconfig1u.shadowed.blue.endless.jankson.JsonPrimitive;
+import me.sargunvohra.mcmods.autoconfig1u.shadowed.blue.endless.jankson.impl.SyntaxError;
 import me.sargunvohra.mcmods.autoconfig1u.util.Utils;
 import me.shedaniel.cloth.api.client.events.v0.ScreenHooks;
 import me.shedaniel.clothconfig2.api.ConfigEntryBuilder;
@@ -45,11 +47,11 @@ import me.shedaniel.clothconfig2.api.Modifier;
 import me.shedaniel.clothconfig2.api.ModifierKeyCode;
 import me.shedaniel.clothconfig2.gui.entries.KeyCodeEntry;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
-import me.shedaniel.rei.RoughlyEnoughItemsState;
 import me.shedaniel.rei.api.ConfigManager;
 import me.shedaniel.rei.api.EntryRegistry;
 import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.REIHelper;
+import me.shedaniel.rei.api.favorites.FavoriteEntry;
 import me.shedaniel.rei.gui.ContainerScreenOverlay;
 import me.shedaniel.rei.gui.WarningAndErrorScreen;
 import me.shedaniel.rei.gui.config.RecipeScreenType;
@@ -83,6 +85,7 @@ import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
 import static me.sargunvohra.mcmods.autoconfig1u.util.Utils.getUnsafely;
 import static me.sargunvohra.mcmods.autoconfig1u.util.Utils.setUnsafely;
@@ -96,6 +99,7 @@ public class ConfigManagerImpl implements ConfigManager {
     
     public ConfigManagerImpl() {
         this.craftableOnly = false;
+        Jankson jankson = Jankson.builder().build();
         AutoConfig.register(ConfigObjectImpl.class, (definition, configClass) -> new JanksonConfigSerializer<>(definition, configClass, Jankson.builder().registerPrimitiveTypeAdapter(InputConstants.Key.class, it -> {
             return it instanceof String ? InputConstants.getKey((String) it) : null;
         }).registerSerializer(InputConstants.Key.class, (it, marshaller) -> new JsonPrimitive(it.getName())).registerTypeAdapter(ModifierKeyCode.class, o -> {
@@ -110,9 +114,29 @@ public class ConfigManagerImpl implements ConfigManager {
             object.put("modifier", new JsonPrimitive(keyCode.getModifier().getValue()));
             return object;
         }).registerSerializer(EntryStack.class, (stack, marshaller) -> {
-            return new JsonPrimitive(gson.toJson(stack.toJson()));
+            try {
+                return jankson.load(gson.toJson(stack.toJson()));
+            } catch (SyntaxError syntaxError) {
+                syntaxError.printStackTrace();
+                return JsonNull.INSTANCE;
+            }
         }).registerPrimitiveTypeAdapter(EntryStack.class, it -> {
             return it instanceof String ? EntryStack.readFromJson(gson.fromJson((String) it, JsonElement.class)) : null;
+        }).registerTypeAdapter(EntryStack.class, it -> {
+            return EntryStack.readFromJson(gson.fromJson(it.toString(), JsonElement.class));
+        }).registerSerializer(FavoriteEntry.class, (favoriteEntry, marshaller) -> {
+            try {
+                return jankson.load(favoriteEntry.toJson(new com.google.gson.JsonObject()).toString());
+            } catch (SyntaxError syntaxError) {
+                syntaxError.printStackTrace();
+                return JsonNull.INSTANCE;
+            }
+        }).registerTypeAdapter(FavoriteEntry.class, it -> {
+            com.google.gson.JsonObject object = gson.fromJson(it.toString(), com.google.gson.JsonObject.class);
+            return FavoriteEntry.delegate(() -> FavoriteEntry.fromJson(object), () -> object);
+        }).registerPrimitiveTypeAdapter(FavoriteEntry.class, it -> {
+            com.google.gson.JsonObject object = gson.fromJson(it.toString(), com.google.gson.JsonObject.class);
+            return FavoriteEntry.delegate(() -> FavoriteEntry.fromJson(object), () -> object);
         }).registerSerializer(FilteringRule.class, (rule, marshaller) -> {
             return new JsonPrimitive(FilteringRule.toTag(rule, new CompoundTag()).toString());
         }).registerPrimitiveTypeAdapter(FilteringRule.class, it -> {
@@ -152,8 +176,8 @@ public class ConfigManagerImpl implements ConfigManager {
     
     @Override
     public void saveConfig() {
-        if (getConfig().getFavorites() != null)
-            getConfig().getFavorites().removeIf(EntryStack::isEmpty);
+        if (getConfig().getFavoriteEntries() != null)
+            getConfig().getFavoriteEntries().removeIf(Objects::isNull);
         if (getConfig().getFilteredStacks() != null) {
             getConfig().getFilteredStacks().removeIf(EntryStack::isEmpty);
             for (EntryStack stack : getConfig().getFilteredStacks()) {

+ 9 - 8
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/ConfigObjectImpl.java

@@ -32,6 +32,7 @@ import me.shedaniel.clothconfig2.api.Modifier;
 import me.shedaniel.clothconfig2.api.ModifierKeyCode;
 import me.shedaniel.rei.api.ConfigObject;
 import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.api.favorites.FavoriteEntry;
 import me.shedaniel.rei.gui.config.*;
 import me.shedaniel.rei.impl.filtering.FilteringRule;
 import net.fabricmc.api.EnvType;
@@ -99,6 +100,11 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
         return basics.cheatingStyle == ItemCheatingStyle.GRAB;
     }
     
+    @Override
+    public boolean isReducedMotion() {
+        return basics.reducedMotion;
+    }
+    
     @Override
     public boolean isToastDisplayedOnCopyIdentifier() {
         return advanced.accessibility.toastDisplayedOnCopyIdentifier;
@@ -229,11 +235,6 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
         return advanced.layout.debugRenderTimeRequired;
     }
     
-    @Override
-    public boolean doSearchFavorites() {
-        return advanced.search.searchFavorites;
-    }
-    
     @Override
     public ModifierKeyCode getFavoriteKeyCode() {
         return basics.keyBindings.favoriteKeybind == null ? ModifierKeyCode.unknown() : basics.keyBindings.favoriteKeybind;
@@ -295,7 +296,7 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
     }
     
     @Override
-    public List<EntryStack> getFavorites() {
+    public List<FavoriteEntry> getFavoriteEntries() {
         return basics.favorites;
     }
     
@@ -363,7 +364,7 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
     }
     
     public static class Basics {
-        @ConfigEntry.Gui.Excluded public List<EntryStack> favorites = new ArrayList<>();
+        @ConfigEntry.Gui.Excluded public List<FavoriteEntry> favorites = new ArrayList<>();
         @Comment("Declares whether cheating mode is on.") private boolean cheating = false;
         private boolean favoritesEnabled = true;
         @ConfigEntry.Gui.CollapsibleObject(startExpanded = true)
@@ -371,6 +372,7 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
         @Comment("Declares whether REI is visible.") @ConfigEntry.Gui.Excluded private boolean overlayVisible = true;
         @ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON)
         private ItemCheatingStyle cheatingStyle = ItemCheatingStyle.GRAB;
+        private boolean reducedMotion = false;
     }
     
     public static class KeyBindings {
@@ -453,7 +455,6 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
         }
         
         public static class Search {
-            @Comment("Declares whether favorites will be searched.") private boolean searchFavorites = false;
             @Comment("Declares whether search time should be debugged.") private boolean debugSearchTimeRequired = false;
             @Comment("Declares whether REI should search async.") private boolean asyncSearch = true;
             @Comment("Declares how many entries should be grouped one async search.") @ConfigEntry.BoundedDiscrete(min = 25, max = 400)

+ 107 - 0
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/FavoriteEntryTypeRegistryImpl.java

@@ -0,0 +1,107 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import com.google.common.collect.Maps;
+import me.shedaniel.rei.api.favorites.FavoriteEntry;
+import me.shedaniel.rei.api.favorites.FavoriteEntryType;
+import net.minecraft.network.chat.Component;
+import net.minecraft.resources.ResourceLocation;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+@ApiStatus.Internal
+public class FavoriteEntryTypeRegistryImpl implements FavoriteEntryType.Registry {
+    private static final FavoriteEntryTypeRegistryImpl INSTANCE = new FavoriteEntryTypeRegistryImpl();
+    private final BiMap<ResourceLocation, FavoriteEntryType<?>> registry = HashBiMap.create();
+    private final Map<Component, FavoriteEntryType.Section> sections = Maps.newLinkedHashMap();
+    
+    public static FavoriteEntryTypeRegistryImpl getInstance() {
+        return INSTANCE;
+    }
+    
+    @Override
+    public void register(ResourceLocation id, FavoriteEntryType<?> type) {
+        this.registry.put(id, type);
+    }
+    
+    @Override
+    public <A extends FavoriteEntry> @Nullable FavoriteEntryType<A> get(ResourceLocation id) {
+        return (FavoriteEntryType<A>) this.registry.get(id);
+    }
+    
+    @Override
+    public @Nullable ResourceLocation getId(FavoriteEntryType<?> type) {
+        return this.registry.inverse().get(type);
+    }
+    
+    @Override
+    public FavoriteEntryType.@NotNull Section getOrCrateSection(Component text) {
+        return sections.computeIfAbsent(text, SectionImpl::new);
+    }
+    
+    @Override
+    public @NotNull Iterable<FavoriteEntryType.Section> sections() {
+        return this.sections.values();
+    }
+    
+    public void clear() {
+        this.registry.clear();
+        this.sections.clear();
+    }
+    
+    private static class SectionImpl implements FavoriteEntryType.Section {
+        private final Component text;
+        private final List<FavoriteEntry> entries = new ArrayList<>();
+        
+        public SectionImpl(Component text) {
+            this.text = text;
+        }
+        
+        @Override
+        public void add(@NotNull FavoriteEntry... entries) {
+            Collections.addAll(this.entries, entries);
+        }
+        
+        @NotNull
+        @Override
+        public Component getText() {
+            return text;
+        }
+        
+        @NotNull
+        @Override
+        public List<FavoriteEntry> getEntries() {
+            return entries;
+        }
+    }
+}

+ 1 - 0
RoughlyEnoughItems-runtime/src/main/java/me/shedaniel/rei/impl/RecipeHelperImpl.java

@@ -363,6 +363,7 @@ public class RecipeHelperImpl implements RecipeHelper {
         DisplayHelperImpl displayHelper = (DisplayHelperImpl) DisplayHelper.getInstance();
         EntryRegistryImpl entryRegistry = (EntryRegistryImpl) EntryRegistry.getInstance();
         
+        FavoriteEntryTypeRegistryImpl.getInstance().clear();
         ((SubsetsRegistryImpl) SubsetsRegistry.getInstance()).reset();
         ((FluidSupportProviderImpl) FluidSupportProvider.getInstance()).reset();
         displayHelper.resetData();

+ 1 - 1
gradle.properties

@@ -1,5 +1,5 @@
 org.gradle.jvmargs=-Xmx3G
-mod_version=5.6.2
+mod_version=5.7.0
 supported_version=1.16.2/3
 minecraft_version=1.16.3
 fabricloader_version=0.9.1+build.205

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

@@ -99,6 +99,8 @@
   "text.rei.view_recipes_for": "View Recipes for %s",
   "text.rei.subsets": "Subsets",
   "text.rei.tiny_potato": "Tiny Potatoz",
+  "text.rei.add_favorite_widget": "Add...",
+  "favorite.section.gamemode": "Game Mode",
   "tooltip.rei.fluid_amount": "§7%d Unit",
   "msg.rei.copied_recipe_id": "Copied Recipe Identifier",
   "msg.rei.recipe_id_details": "Recipe ID: %s",
@@ -127,6 +129,7 @@
   "config.roughlyenoughitems.cheatingStyle": "Cheating Style:",
   "config.roughlyenoughitems.cheatingStyle.grab": "Grab",
   "config.roughlyenoughitems.cheatingStyle.give": "Give",
+  "config.roughlyenoughitems.reducedMotion": "Reduced Motion:",
   "config.roughlyenoughitems.recipeScreenType": "Recipe Screen Type:",
   "config.roughlyenoughitems.recipeScreenType.config": "Recipe Screen Type: %s",
   "config.roughlyenoughitems.recipeScreenType.unset": "Not Set",