ソースを参照

Merge branch 'fabric-dev' into 1.14

Daniel She 6 年 前
コミット
760fa64d6a
40 ファイル変更597 行追加204 行削除
  1. 10 9
      build.gradle
  2. 1 1
      gradle.properties
  3. 5 1
      src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCore.java
  4. 1 1
      src/main/java/me/shedaniel/rei/api/DisplayVisibility.java
  5. 0 1
      src/main/java/me/shedaniel/rei/api/DisplayVisibilityHandler.java
  6. 8 2
      src/main/java/me/shedaniel/rei/api/RecipeCategory.java
  7. 2 1
      src/main/java/me/shedaniel/rei/api/RecipeDisplay.java
  8. 40 2
      src/main/java/me/shedaniel/rei/api/RecipeHelper.java
  9. 6 4
      src/main/java/me/shedaniel/rei/client/ConfigObject.java
  10. 83 10
      src/main/java/me/shedaniel/rei/client/RecipeHelperImpl.java
  11. 7 7
      src/main/java/me/shedaniel/rei/client/ScreenHelper.java
  12. 40 41
      src/main/java/me/shedaniel/rei/gui/ContainerScreenOverlay.java
  13. 46 6
      src/main/java/me/shedaniel/rei/gui/RecipeViewingScreen.java
  14. 131 1
      src/main/java/me/shedaniel/rei/gui/VillagerRecipeViewingScreen.java
  15. 36 9
      src/main/java/me/shedaniel/rei/gui/renderables/SimpleRecipeRenderer.java
  16. 3 1
      src/main/java/me/shedaniel/rei/gui/widget/ButtonWidget.java
  17. 3 3
      src/main/java/me/shedaniel/rei/gui/widget/ClickableLabelWidget.java
  18. 35 39
      src/main/java/me/shedaniel/rei/gui/widget/ItemListOverlay.java
  19. 4 0
      src/main/java/me/shedaniel/rei/gui/widget/ItemSlotWidget.java
  20. 13 0
      src/main/java/me/shedaniel/rei/gui/widget/QueuedTooltip.java
  21. 11 2
      src/main/java/me/shedaniel/rei/gui/widget/RecipeBaseWidget.java
  22. 3 1
      src/main/java/me/shedaniel/rei/gui/widget/SlotBaseWidget.java
  23. 31 8
      src/main/java/me/shedaniel/rei/gui/widget/SlotWidget.java
  24. 10 0
      src/main/java/me/shedaniel/rei/gui/widget/SpeedCraftingButtonWidget.java
  25. 6 2
      src/main/java/me/shedaniel/rei/gui/widget/TabWidget.java
  26. 2 3
      src/main/java/me/shedaniel/rei/plugin/DefaultBlastingCategory.java
  27. 1 3
      src/main/java/me/shedaniel/rei/plugin/DefaultBrewingCategory.java
  28. 1 3
      src/main/java/me/shedaniel/rei/plugin/DefaultCampfireCategory.java
  29. 1 3
      src/main/java/me/shedaniel/rei/plugin/DefaultCraftingCategory.java
  30. 25 24
      src/main/java/me/shedaniel/rei/plugin/DefaultPlugin.java
  31. 1 3
      src/main/java/me/shedaniel/rei/plugin/DefaultSmeltingCategory.java
  32. 1 3
      src/main/java/me/shedaniel/rei/plugin/DefaultSmokingCategory.java
  33. 1 3
      src/main/java/me/shedaniel/rei/plugin/DefaultStoneCuttingCategory.java
  34. 3 3
      src/main/java/me/shedaniel/rei/utils/ClothScreenRegistry.java
  35. 6 2
      src/main/resources/assets/roughlyenoughitems/lang/en_us.json
  36. 20 2
      src/main/resources/assets/roughlyenoughitems/lang/zh_cn.json
  37. BIN
      src/main/resources/assets/roughlyenoughitems/textures/gui/button_dark.png
  38. BIN
      src/main/resources/assets/roughlyenoughitems/textures/gui/display_dark.png
  39. BIN
      src/main/resources/assets/roughlyenoughitems/textures/gui/recipecontainer.png
  40. BIN
      src/main/resources/assets/roughlyenoughitems/textures/gui/recipecontainer_dark.png

+ 10 - 9
build.gradle

@@ -56,6 +56,7 @@ dependencies {
     }
     modCompile "io.github.prospector.modmenu:ModMenu:${modmenu_version}"
     compile "org.lwjgl:lwjgl-jemalloc:3.2.1"
+    compileOnly "com.google.code.findbugs:jsr305:3.0.2"
 }
 
 task sourcesJar(type: Jar, dependsOn: classes) {
@@ -76,14 +77,14 @@ publishing {
 	}
 
 	repositories {
-		if (project.hasProperty('danielshe_pass')) {
-			maven {
-				url = "http://deploy.modmuss50.me/"
-				credentials {
-					username = "danielshe"
-					password = project.getProperty('danielshe_pass')
-				}
-			}
-		}
+//		if (project.hasProperty('danielshe_pass')) {
+//			maven {
+//				url = "http://deploy.modmuss50.me/"
+//				credentials {
+//					username = "danielshe"
+//					password = project.getProperty('danielshe_pass')
+//				}
+//			}
+//		}
 	}
 }

+ 1 - 1
gradle.properties

@@ -1,4 +1,4 @@
-mod_version=2.9.2
+mod_version=2.9.3
 minecraft_version=1.14.2
 yarn_version=1.14.2+build.2
 fabricloader_version=0.4.7+build.147

+ 5 - 1
src/main/java/me/shedaniel/rei/RoughlyEnoughItemsCore.java

@@ -42,6 +42,7 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
 import java.util.stream.Collectors;
 
 public class RoughlyEnoughItemsCore implements ClientModInitializer {
@@ -206,7 +207,10 @@ public class RoughlyEnoughItemsCore implements ClientModInitializer {
     
     private void registerClothEvents() {
         ClothClientHooks.SYNC_RECIPES.register((minecraftClient, recipeManager, synchronizeRecipesS2CPacket) -> {
-            ((RecipeHelperImpl) RoughlyEnoughItemsCore.getRecipeHelper()).recipesLoaded(recipeManager);
+            if (RoughlyEnoughItemsCore.getConfigManager().getConfig().registerRecipesInAnotherThread)
+                CompletableFuture.runAsync(() -> ((RecipeHelperImpl) RoughlyEnoughItemsCore.getRecipeHelper()).recipesLoaded(recipeManager));
+            else
+                ((RecipeHelperImpl) RoughlyEnoughItemsCore.getRecipeHelper()).recipesLoaded(recipeManager);
         });
         ClothClientHooks.SCREEN_ADD_BUTTON.register((minecraftClient, screen, abstractButtonWidget) -> {
             if (RoughlyEnoughItemsCore.getConfigManager().getConfig().disableRecipeBook && screen instanceof AbstractContainerScreen && abstractButtonWidget instanceof RecipeBookButtonWidget)

+ 1 - 1
src/main/java/me/shedaniel/rei/api/DisplayVisibility.java

@@ -7,7 +7,7 @@ package me.shedaniel.rei.api;
 
 public enum DisplayVisibility {
     ALWAYS_VISIBLE,
-    CONFIG_OPTIONAL,
+    @Deprecated CONFIG_OPTIONAL,
     NEVER_VISIBLE,
     PASS
 }

+ 0 - 1
src/main/java/me/shedaniel/rei/api/DisplayVisibilityHandler.java

@@ -20,7 +20,6 @@ public interface DisplayVisibilityHandler {
      * Handles the visibility of the display.
      * {@link DisplayVisibility#PASS} to pass the handling to another handler
      * {@link DisplayVisibility#ALWAYS_VISIBLE} to always display it
-     * {@link DisplayVisibility#CONFIG_OPTIONAL} to allow user to configure the visibility
      * {@link DisplayVisibility#NEVER_VISIBLE} to never display it
      *
      * @param category the category of the display

+ 8 - 2
src/main/java/me/shedaniel/rei/api/RecipeCategory.java

@@ -5,6 +5,7 @@
 
 package me.shedaniel.rei.api;
 
+import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.gui.RecipeViewingScreen;
 import me.shedaniel.rei.gui.renderables.RecipeRenderer;
 import me.shedaniel.rei.gui.widget.CategoryBaseWidget;
@@ -87,8 +88,13 @@ public interface RecipeCategory<T extends RecipeDisplay> {
      */
     default void drawCategoryBackground(Rectangle bounds, int mouseX, int mouseY, float delta) {
         new CategoryBaseWidget(bounds).render();
-        DrawableHelper.fill(bounds.x + 17, bounds.y + 5, bounds.x + bounds.width - 17, bounds.y + 17, RecipeViewingScreen.SUB_COLOR.getRGB());
-        DrawableHelper.fill(bounds.x + 17, bounds.y + 21, bounds.x + bounds.width - 17, bounds.y + 33, RecipeViewingScreen.SUB_COLOR.getRGB());
+        if (RoughlyEnoughItemsCore.getConfigManager().getConfig().darkTheme) {
+            DrawableHelper.fill(bounds.x + 17, bounds.y + 5, bounds.x + bounds.width - 17, bounds.y + 17, 0xFF404040);
+            DrawableHelper.fill(bounds.x + 17, bounds.y + 21, bounds.x + bounds.width - 17, bounds.y + 33, 0xFF404040);
+        } else {
+            DrawableHelper.fill(bounds.x + 17, bounds.y + 5, bounds.x + bounds.width - 17, bounds.y + 17, 0xFF9E9E9E);
+            DrawableHelper.fill(bounds.x + 17, bounds.y + 21, bounds.x + bounds.width - 17, bounds.y + 33, 0xFF9E9E9E);
+        }
     }
     
     /**

+ 2 - 1
src/main/java/me/shedaniel/rei/api/RecipeDisplay.java

@@ -18,7 +18,7 @@ public interface RecipeDisplay<T extends Recipe> {
     /**
      * @return the optional recipe
      */
-    Optional<T> getRecipe();
+    Optional<? extends Recipe> getRecipe();
     
     /**
      * @return a list of items
@@ -35,6 +35,7 @@ public interface RecipeDisplay<T extends Recipe> {
      *
      * @return the list of required items
      */
+    @Deprecated
     default List<List<ItemStack>> getRequiredItems() {
         return Lists.newArrayList();
     }

+ 40 - 2
src/main/java/me/shedaniel/rei/api/RecipeHelper.java

@@ -14,6 +14,7 @@ import net.minecraft.util.Identifier;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.function.Function;
 
 public interface RecipeHelper {
     
@@ -51,6 +52,24 @@ public interface RecipeHelper {
      */
     void registerCategory(RecipeCategory category);
     
+    /**
+     * Registers the working stations of a category
+     *
+     * @param category        the category
+     * @param workingStations the working stations
+     */
+    void registerWorkingStations(Identifier category, List<ItemStack>... workingStations);
+    
+    /**
+     * Registers the working stations of a category
+     *
+     * @param category        the category
+     * @param workingStations the working stations
+     */
+    void registerWorkingStations(Identifier category, ItemStack... workingStations);
+    
+    List<List<ItemStack>> getWorkingStations(Identifier category);
+    
     /**
      * Registers a recipe display
      *
@@ -106,10 +125,10 @@ public interface RecipeHelper {
     void registerSpeedCraftButtonArea(Identifier category, ButtonAreaSupplier rectangle);
     
     /**
-     * Registers a default speed crafting button area, which is bottom right
-     *
      * @param category the category of the button area
+     * @deprecated Not required anymore
      */
+    @Deprecated
     void registerDefaultSpeedCraftButtonArea(Identifier category);
     
     /**
@@ -162,9 +181,19 @@ public interface RecipeHelper {
      * @param display       the display to be checked
      * @param respectConfig whether it should respect the user's config
      * @return whether the display should be visible
+     * @deprecated {@link RecipeHelper#isDisplayVisible(RecipeDisplay)} )}
      */
+    @Deprecated
     boolean isDisplayVisible(RecipeDisplay display, boolean respectConfig);
     
+    /**
+     * Checks if the display is visible by asking recipe visibility handlers
+     *
+     * @param display the display to be checked
+     * @return whether the display should be visible
+     */
+    boolean isDisplayVisible(RecipeDisplay display);
+    
     /**
      * Gets the cached category setting by the category identifier
      *
@@ -173,6 +202,15 @@ public interface RecipeHelper {
      */
     Optional<DisplaySettings> getCachedCategorySettings(Identifier category);
     
+    /**
+     * Registers a live recipe generator.
+     *
+     * @param liveRecipeGenerator the generator to register
+     * @apiNote Still work in progress
+     */
     void registerLiveRecipeGenerator(LiveRecipeGenerator liveRecipeGenerator);
     
+    <T extends Recipe<?>> void registerRecipes(Identifier category, Class<T> recipeClass, Function<T, RecipeDisplay> mappingFunction);
+    
+    <T extends Recipe<?>> void registerRecipes(Identifier category, Function<Recipe, Boolean> recipeFilter, Function<T, RecipeDisplay> mappingFunction);
 }

+ 6 - 4
src/main/java/me/shedaniel/rei/client/ConfigObject.java

@@ -42,14 +42,16 @@ public class ConfigObject {
     
     @Comment("Disable Recipe Book") public boolean disableRecipeBook = false;
     
-    public boolean preferVisibleRecipes = false;
-    
-    @Comment("Force enable 2019 REI April Fools' joke") public boolean aprilFoolsFish2019 = false;
-    
     public ItemCheatingMode itemCheatingMode = ItemCheatingMode.REI_LIKE;
     
     public boolean lightGrayRecipeBorder = false;
     
+    public boolean villagerScreenPermanentScrollBar = false;
+    
+    public boolean darkTheme = false;
+    
+    public boolean registerRecipesInAnotherThread = true;
+    
     public RecipeScreenType screenType = RecipeScreenType.UNSET;
     
     @Comment(

+ 83 - 10
src/main/java/me/shedaniel/rei/client/RecipeHelperImpl.java

@@ -18,6 +18,7 @@ import java.awt.*;
 import java.util.List;
 import java.util.*;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 
 public class RecipeHelperImpl implements RecipeHelper {
@@ -35,12 +36,14 @@ public class RecipeHelperImpl implements RecipeHelper {
         VISIBILITY_HANDLER_COMPARATOR = comparator.reversed();
     }
     
+    public final List<RecipeFunction> recipeFunctions = Lists.newArrayList();
     private final AtomicInteger recipeCount = new AtomicInteger();
     private final Map<Identifier, List<RecipeDisplay>> recipeCategoryListMap = Maps.newHashMap();
     private final Map<Identifier, DisplaySettings> categoryDisplaySettingsMap = Maps.newHashMap();
     private final List<RecipeCategory> categories = Lists.newArrayList();
     private final Map<Identifier, ButtonAreaSupplier> speedCraftAreaSupplierMap = Maps.newHashMap();
     private final Map<Identifier, List<SpeedCraftFunctional>> speedCraftFunctionalMap = Maps.newHashMap();
+    private final Map<Identifier, List<List<ItemStack>>> categoryWorkingStations = Maps.newHashMap();
     private final List<DisplayVisibilityHandler> displayVisibilityHandlers = Lists.newArrayList();
     private final List<LiveRecipeGenerator> liveRecipeGenerators = Lists.newArrayList();
     private RecipeManager recipeManager;
@@ -80,6 +83,22 @@ public class RecipeHelperImpl implements RecipeHelper {
         categories.add(category);
         categoryDisplaySettingsMap.put(category.getIdentifier(), category.getDisplaySettings());
         recipeCategoryListMap.put(category.getIdentifier(), Lists.newLinkedList());
+        categoryWorkingStations.put(category.getIdentifier(), Lists.newLinkedList());
+    }
+    
+    @Override
+    public void registerWorkingStations(Identifier category, List<ItemStack>... workingStations) {
+        categoryWorkingStations.get(category).addAll(Arrays.asList(workingStations));
+    }
+    
+    @Override
+    public void registerWorkingStations(Identifier category, ItemStack... workingStations) {
+        categoryWorkingStations.get(category).addAll(Arrays.asList(workingStations).stream().map(Collections::singletonList).collect(Collectors.toList()));
+    }
+    
+    @Override
+    public List<List<ItemStack>> getWorkingStations(Identifier category) {
+        return categoryWorkingStations.get(category);
     }
     
     @Override
@@ -90,6 +109,13 @@ public class RecipeHelperImpl implements RecipeHelper {
         recipeCategoryListMap.get(categoryIdentifier).add(display);
     }
     
+    private void registerDisplay(Identifier categoryIdentifier, RecipeDisplay display, int index) {
+        if (!recipeCategoryListMap.containsKey(categoryIdentifier))
+            return;
+        recipeCount.incrementAndGet();
+        recipeCategoryListMap.get(categoryIdentifier).add(index, display);
+    }
+    
     @Override
     public Map<RecipeCategory, List<RecipeDisplay>> getRecipesFor(ItemStack stack) {
         Map<Identifier, List<RecipeDisplay>> categoriesMap = new HashMap<>();
@@ -106,7 +132,7 @@ public class RecipeHelperImpl implements RecipeHelper {
         Map<RecipeCategory, List<RecipeDisplay>> recipeCategoryListMap = Maps.newLinkedHashMap();
         categories.forEach(category -> {
             if (categoriesMap.containsKey(category.getIdentifier()) && !categoriesMap.get(category.getIdentifier()).isEmpty())
-                recipeCategoryListMap.put(category, categoriesMap.get(category.getIdentifier()).stream().filter(display -> isDisplayVisible(display, true)).collect(Collectors.toList()));
+                recipeCategoryListMap.put(category, categoriesMap.get(category.getIdentifier()).stream().filter(display -> isDisplayVisible(display)).collect(Collectors.toList()));
         });
         for(RecipeCategory category : Lists.newArrayList(recipeCategoryListMap.keySet()))
             if (recipeCategoryListMap.get(category).isEmpty())
@@ -149,7 +175,7 @@ public class RecipeHelperImpl implements RecipeHelper {
         Map<RecipeCategory, List<RecipeDisplay>> recipeCategoryListMap = Maps.newLinkedHashMap();
         categories.forEach(category -> {
             if (categoriesMap.containsKey(category.getIdentifier()) && !categoriesMap.get(category.getIdentifier()).isEmpty())
-                recipeCategoryListMap.put(category, categoriesMap.get(category.getIdentifier()).stream().filter(display -> isDisplayVisible(display, true)).collect(Collectors.toList()));
+                recipeCategoryListMap.put(category, categoriesMap.get(category.getIdentifier()).stream().filter(display -> isDisplayVisible(display)).collect(Collectors.toList()));
         });
         for(RecipeCategory category : Lists.newArrayList(recipeCategoryListMap.keySet()))
             if (recipeCategoryListMap.get(category).isEmpty())
@@ -165,13 +191,17 @@ public class RecipeHelperImpl implements RecipeHelper {
     @Override
     public Optional<ButtonAreaSupplier> getSpeedCraftButtonArea(RecipeCategory category) {
         if (!speedCraftAreaSupplierMap.containsKey(category.getIdentifier()))
-            return Optional.empty();
+            return Optional.ofNullable(bounds -> new Rectangle((int) bounds.getMaxX() - 16, (int) bounds.getMaxY() - 16, 10, 10));
         return Optional.ofNullable(speedCraftAreaSupplierMap.get(category.getIdentifier()));
     }
     
     @Override
     public void registerSpeedCraftButtonArea(Identifier category, ButtonAreaSupplier rectangle) {
-        speedCraftAreaSupplierMap.put(category, rectangle);
+        if (rectangle == null) {
+            if (speedCraftAreaSupplierMap.containsKey(category))
+                speedCraftAreaSupplierMap.remove(category);
+        } else
+            speedCraftAreaSupplierMap.put(category, rectangle);
     }
     
     @Override
@@ -200,8 +230,10 @@ public class RecipeHelperImpl implements RecipeHelper {
         this.recipeCategoryListMap.clear();
         this.categories.clear();
         this.speedCraftAreaSupplierMap.clear();
+        this.categoryWorkingStations.clear();
         this.speedCraftFunctionalMap.clear();
         this.categoryDisplaySettingsMap.clear();
+        this.recipeFunctions.clear();
         this.displayVisibilityHandlers.clear();
         this.liveRecipeGenerators.clear();
         ((DisplayHelperImpl) RoughlyEnoughItemsCore.getDisplayHelper()).resetCache();
@@ -234,6 +266,17 @@ public class RecipeHelperImpl implements RecipeHelper {
                 RoughlyEnoughItemsCore.LOGGER.error("[REI] " + identifier.toString() + " plugin failed to load!", e);
             }
         });
+        if (!recipeFunctions.isEmpty()) {
+            List<Recipe> allSortedRecipes = getAllSortedRecipes();
+            Collections.reverse(allSortedRecipes);
+            recipeFunctions.forEach(recipeFunction -> {
+                try {
+                    allSortedRecipes.stream().filter(recipe -> recipeFunction.recipeFilter.apply(recipe)).forEach(t -> registerDisplay(recipeFunction.category, (RecipeDisplay) recipeFunction.mappingFunction.apply(t), 0));
+                } catch (Exception e) {
+                    RoughlyEnoughItemsCore.LOGGER.error("[REI] Failed to add recipes!", e);
+                }
+            });
+        }
         if (getDisplayVisibilityHandlers().isEmpty())
             registerRecipeVisibilityHandler(new DisplayVisibilityHandler() {
                 @Override
@@ -265,7 +308,7 @@ public class RecipeHelperImpl implements RecipeHelper {
         Map<RecipeCategory, List<RecipeDisplay>> map = Maps.newLinkedHashMap();
         categories.forEach(recipeCategory -> {
             if (recipeCategoryListMap.containsKey(recipeCategory.getIdentifier())) {
-                List<RecipeDisplay> list = recipeCategoryListMap.get(recipeCategory.getIdentifier()).stream().filter(display -> isDisplayVisible(display, true)).collect(Collectors.toList());
+                List<RecipeDisplay> list = recipeCategoryListMap.get(recipeCategory.getIdentifier()).stream().filter(display -> isDisplayVisible(display)).collect(Collectors.toList());
                 if (!list.isEmpty())
                     map.put(recipeCategory, list);
             }
@@ -288,21 +331,39 @@ public class RecipeHelperImpl implements RecipeHelper {
         return Collections.unmodifiableList(displayVisibilityHandlers);
     }
     
+    @SuppressWarnings("deprecation")
     @Override
     public boolean isDisplayVisible(RecipeDisplay display, boolean respectConfig) {
+        return isDisplayVisible(display);
+    }
+    
+    @SuppressWarnings("deprecation")
+    @Override
+    public boolean isDisplayVisible(RecipeDisplay display) {
         RecipeCategory category = getCategory(display.getRecipeCategory());
         List<DisplayVisibilityHandler> list = getDisplayVisibilityHandlers().stream().sorted(VISIBILITY_HANDLER_COMPARATOR).collect(Collectors.toList());
         for(DisplayVisibilityHandler displayVisibilityHandler : list) {
-            DisplayVisibility visibility = displayVisibilityHandler.handleDisplay(category, display);
-            if (visibility != DisplayVisibility.PASS) {
-                if (visibility == DisplayVisibility.CONFIG_OPTIONAL)
-                    return RoughlyEnoughItemsCore.getConfigManager().getConfig().preferVisibleRecipes || !respectConfig;
-                return visibility == DisplayVisibility.ALWAYS_VISIBLE;
+            try {
+                DisplayVisibility visibility = displayVisibilityHandler.handleDisplay(category, display);
+                if (visibility != DisplayVisibility.PASS)
+                    return visibility == DisplayVisibility.ALWAYS_VISIBLE || visibility == DisplayVisibility.CONFIG_OPTIONAL;
+            } catch (Throwable throwable) {
+                RoughlyEnoughItemsCore.LOGGER.error("[REI] Failed to check if the recipe is visible!", throwable);
             }
         }
         return true;
     }
     
+    @Override
+    public <T extends Recipe<?>> void registerRecipes(Identifier category, Class<T> recipeClass, Function<T, RecipeDisplay> mappingFunction) {
+        recipeFunctions.add(new RecipeFunction(category, recipe -> recipeClass.isAssignableFrom(recipe.getClass()), mappingFunction));
+    }
+    
+    @Override
+    public <T extends Recipe<?>> void registerRecipes(Identifier category, Function<Recipe, Boolean> recipeFilter, Function<T, RecipeDisplay> mappingFunction) {
+        recipeFunctions.add(new RecipeFunction(category, recipeFilter, mappingFunction));
+    }
+    
     @Override
     public Optional<DisplaySettings> getCachedCategorySettings(Identifier category) {
         return categoryDisplaySettingsMap.entrySet().stream().filter(entry -> entry.getKey().equals(category)).map(Map.Entry::getValue).findAny();
@@ -313,4 +374,16 @@ public class RecipeHelperImpl implements RecipeHelper {
         liveRecipeGenerators.add(liveRecipeGenerator);
     }
     
+    private class RecipeFunction {
+        Identifier category;
+        Function<Recipe, Boolean> recipeFilter;
+        Function mappingFunction;
+        
+        public RecipeFunction(Identifier category, Function<Recipe, Boolean> recipeFilter, Function<?, RecipeDisplay> mappingFunction) {
+            this.category = category;
+            this.recipeFilter = recipeFilter;
+            this.mappingFunction = mappingFunction;
+        }
+    }
+    
 }

+ 7 - 7
src/main/java/me/shedaniel/rei/client/ScreenHelper.java

@@ -66,13 +66,13 @@ public class ScreenHelper implements ClientModInitializer {
     }
     
     public static void drawHoveringWidget(Dimension dimension, int x, int y, TriConsumer<Integer, Integer, Float> consumer, int width, int height, float delta) {
-        int int_5 = x + 12;
-        int int_6 = y - 12;
-        if (int_5 + width > dimension.width)
-            int_5 -= 28 + width;
-        if (int_6 + height + 6 > dimension.height)
-            int_6 = dimension.height - height - 6;
-        consumer.accept(int_5, int_6, delta);
+        int actualX = Math.max(x + 12, 6);
+        int actualY = Math.min(y - height / 2, dimension.height - height - 6);
+        if (actualX + width > dimension.width)
+            actualX -= 24 + width;
+        if (actualY < 6)
+            actualY += 24;
+        consumer.accept(actualX, actualY, delta);
     }
     
     @Override

+ 40 - 41
src/main/java/me/shedaniel/rei/gui/ContainerScreenOverlay.java

@@ -183,31 +183,35 @@ public class ContainerScreenOverlay extends AbstractParentElement implements Dra
                     return false;
                 }
             });
-            widgets.add(new ButtonWidget(RoughlyEnoughItemsCore.getConfigManager().getConfig().mirrorItemPanel ? window.getScaledWidth() - 80 : 60, 10, 20, 20, "") {
-                @Override
-                public void onPressed() {
-                    MinecraftClient.getInstance().player.sendChatMessage(RoughlyEnoughItemsCore.getConfigManager().getConfig().weatherCommand.replaceAll("\\{weather}", getNextWeather().name().toLowerCase()));
-                }
-                
-                @Override
-                public void render(int mouseX, int mouseY, float delta) {
-                    super.render(mouseX, mouseY, delta);
-                    GuiLighting.disable();
-                    MinecraftClient.getInstance().getTextureManager().bindTexture(CHEST_GUI_TEXTURE);
-                    GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
-                    blit(getBounds().x + 3, getBounds().y + 3, getCurrentWeather().getId() * 14, 14, 14, 14);
-                }
-                
-                @Override
-                public Optional<String> getTooltips() {
-                    return Optional.ofNullable(I18n.translate("text.rei.weather_button.tooltip", I18n.translate(getNextWeather().getTranslateKey())));
-                }
-                
-                @Override
-                public boolean changeFocus(boolean boolean_1) {
-                    return false;
-                }
-            });
+            int xxx = RoughlyEnoughItemsCore.getConfigManager().getConfig().mirrorItemPanel ? window.getScaledWidth() -30 : 10;
+            for(Weather weather : Weather.values()) {
+                widgets.add(new ButtonWidget(xxx, 35, 20, 20, "") {
+                    @Override
+                    public void onPressed() {
+                        MinecraftClient.getInstance().player.sendChatMessage(RoughlyEnoughItemsCore.getConfigManager().getConfig().weatherCommand.replaceAll("\\{weather}", weather.name().toLowerCase()));
+                    }
+                    
+                    @Override
+                    public void render(int mouseX, int mouseY, float delta) {
+                        super.render(mouseX, mouseY, delta);
+                        GuiLighting.disable();
+                        MinecraftClient.getInstance().getTextureManager().bindTexture(CHEST_GUI_TEXTURE);
+                        GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
+                        blit(getBounds().x + 3, getBounds().y + 3, weather.getId() * 14, 14, 14, 14);
+                    }
+                    
+                    @Override
+                    public Optional<String> getTooltips() {
+                        return Optional.ofNullable(I18n.translate("text.rei.weather_button.tooltip", I18n.translate(weather.getTranslateKey())));
+                    }
+                    
+                    @Override
+                    public boolean changeFocus(boolean boolean_1) {
+                        return false;
+                    }
+                });
+                xxx += RoughlyEnoughItemsCore.getConfigManager().getConfig().mirrorItemPanel ? -25 : 25;
+            }
         }
         widgets.add(new ClickableLabelWidget(rectangle.x + (rectangle.width / 2), rectangle.y + 10, "", getTotalPage() > 0) {
             @Override
@@ -373,28 +377,24 @@ public class ContainerScreenOverlay extends AbstractParentElement implements Dra
         QUEUED_TOOLTIPS.clear();
     }
     
+    @SuppressWarnings("deprecation")
     public void renderTooltip(QueuedTooltip tooltip) {
-        renderTooltip(tooltip.getText(), tooltip.getX(), tooltip.getY());
+        if (tooltip.getConsumer() == null)
+            renderTooltip(tooltip.getText(), tooltip.getX(), tooltip.getY());
+        else
+            tooltip.getConsumer().accept(tooltip);
     }
     
     public void renderTooltip(List<String> lines, int mouseX, int mouseY) {
+        if (lines.isEmpty())
+            return;
         TextRenderer font = MinecraftClient.getInstance().textRenderer;
-        if (!lines.isEmpty()) {
+        int width = lines.stream().map(font::getStringWidth).max(Integer::compareTo).get();
+        int height = lines.size() <= 1 ? 8 : lines.size() * 10;
+        ScreenHelper.drawHoveringWidget(mouseX, mouseY, (x, y, aFloat) -> {
             GlStateManager.disableRescaleNormal();
             GuiLighting.disable();
             GlStateManager.disableLighting();
-            int width = 0;
-            for(String line : lines)
-                if (font.getStringWidth(line) > width)
-                    width = font.getStringWidth(line);
-            int height = lines.size() <= 1 ? 8 : lines.size() * 10;
-            int x = Math.max(mouseX + 12, 6);
-            int y = Math.min(mouseY - 12, window.getScaledHeight() - height - 6);
-            if (x + width > window.getScaledWidth())
-                x -= 24 + width;
-            if (y < 6)
-                y += 24;
-            
             this.blitOffset = 1000;
             this.fillGradient(x - 3, y - 4, x + width + 3, y - 3, -267386864, -267386864);
             this.fillGradient(x - 3, y + height + 3, x + width + 3, y + height + 4, -267386864, -267386864);
@@ -405,7 +405,6 @@ public class ContainerScreenOverlay extends AbstractParentElement implements Dra
             this.fillGradient(x + width + 2, y - 3 + 1, x + width + 3, y + height + 3 - 1, 1347420415, 1344798847);
             this.fillGradient(x - 3, y - 3, x + width + 3, y - 3 + 1, 1347420415, 1347420415);
             this.fillGradient(x - 3, y + height + 2, x + width + 3, y + height + 3, 1344798847, 1344798847);
-            
             int currentY = y;
             for(int lineIndex = 0; lineIndex < lines.size(); lineIndex++) {
                 GlStateManager.disableDepthTest();
@@ -417,7 +416,7 @@ public class ContainerScreenOverlay extends AbstractParentElement implements Dra
             GlStateManager.enableLighting();
             GuiLighting.enable();
             GlStateManager.enableRescaleNormal();
-        }
+        }, width, height, 0);
     }
     
     private boolean hasSameListContent(List<ItemStack> list1, List<ItemStack> list2) {

+ 46 - 6
src/main/java/me/shedaniel/rei/gui/RecipeViewingScreen.java

@@ -12,6 +12,7 @@ import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.*;
 import me.shedaniel.rei.client.ScreenHelper;
 import me.shedaniel.rei.gui.widget.*;
+import net.minecraft.ChatFormat;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.Element;
 import net.minecraft.client.gui.screen.Screen;
@@ -20,6 +21,7 @@ import net.minecraft.client.render.GuiLighting;
 import net.minecraft.client.resource.language.I18n;
 import net.minecraft.client.sound.PositionedSoundInstance;
 import net.minecraft.client.util.Window;
+import net.minecraft.item.ItemStack;
 import net.minecraft.network.chat.TextComponent;
 import net.minecraft.network.chat.TranslatableComponent;
 import net.minecraft.sound.SoundEvents;
@@ -27,17 +29,15 @@ import net.minecraft.util.Identifier;
 import net.minecraft.util.math.MathHelper;
 
 import java.awt.*;
-import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
 import java.util.function.Supplier;
 
 public class RecipeViewingScreen extends Screen {
     
     public static final Identifier CHEST_GUI_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
-    public static final Color SUB_COLOR = new Color(159, 159, 159);
     private static final int TABS_PER_PAGE = 5;
+    private final List<Widget> preWidgets;
     private final List<Widget> widgets;
     private final List<TabWidget> tabs;
     private final Map<RecipeCategory, List<RecipeDisplay>> categoriesMap;
@@ -55,6 +55,7 @@ public class RecipeViewingScreen extends Screen {
     public RecipeViewingScreen(Map<RecipeCategory, List<RecipeDisplay>> categoriesMap) {
         super(new TextComponent(""));
         this.categoryPages = 0;
+        this.preWidgets = Lists.newArrayList();
         this.widgets = Lists.newArrayList();
         Window window = MinecraftClient.getInstance().window;
         this.bounds = new Rectangle(window.getScaledWidth() / 2 - guiWidth / 2, window.getScaledHeight() / 2 - guiHeight / 2, 176, 186);
@@ -122,6 +123,7 @@ public class RecipeViewingScreen extends Screen {
         super.init();
         this.children.clear();
         this.tabs.clear();
+        this.preWidgets.clear();
         this.widgets.clear();
         this.largestWidth = width - 100;
         this.largestHeight = height - 40;
@@ -295,9 +297,38 @@ public class RecipeViewingScreen extends Screen {
         else
             recipeChoosePageWidget = null;
         
+        List<List<ItemStack>> workingStations = RoughlyEnoughItemsCore.getRecipeHelper().getWorkingStations(selectedCategory.getIdentifier());
+        if (!workingStations.isEmpty()) {
+            int hh = MathHelper.floor((bounds.height - 16) / 18f);
+            int actualHeight = Math.min(hh, workingStations.size());
+            int innerWidth = MathHelper.ceil(workingStations.size() / ((float) hh));
+            int xx = bounds.x - (10 + innerWidth * 18) + 6;
+            int yy = bounds.y + 16;
+            preWidgets.add(new CategoryBaseWidget(new Rectangle(xx - 6, yy - 6, 15 + innerWidth * 18, 11 + actualHeight * 18)));
+            int index = 0;
+            List list = Collections.singletonList(ChatFormat.YELLOW.toString() + I18n.translate("text.rei.working_station"));
+            xx += (innerWidth - 1) * 18;
+            for(List<ItemStack> workingStation : workingStations) {
+                preWidgets.add(new SlotWidget(xx, yy, workingStation, true, true, true) {
+                    @Override
+                    protected List<String> getExtraToolTips(ItemStack stack) {
+                        return list;
+                    }
+                });
+                index++;
+                yy += 18;
+                if (index >= hh) {
+                    index = 0;
+                    yy = bounds.y + 16;
+                    xx -= 18;
+                }
+            }
+        }
+        
         children.addAll(tabs);
         children.add(ScreenHelper.getLastOverlay(true, false));
         children.addAll(widgets);
+        children.addAll(preWidgets);
     }
     
     public List<Widget> getWidgets() {
@@ -340,12 +371,21 @@ public class RecipeViewingScreen extends Screen {
     @Override
     public void render(int mouseX, int mouseY, float delta) {
         this.fillGradient(0, 0, this.width, this.height, -1072689136, -804253680);
+        preWidgets.forEach(widget -> {
+            GuiLighting.disable();
+            widget.render(mouseX, mouseY, delta);
+        });
         if (selectedCategory != null)
             selectedCategory.drawCategoryBackground(bounds, mouseX, mouseY, delta);
         else {
             new CategoryBaseWidget(bounds).render();
-            fill(bounds.x + 17, bounds.y + 5, bounds.x + bounds.width - 17, bounds.y + 17, SUB_COLOR.getRGB());
-            fill(bounds.x + 17, bounds.y + 21, bounds.x + bounds.width - 17, bounds.y + 33, SUB_COLOR.getRGB());
+            if (RoughlyEnoughItemsCore.getConfigManager().getConfig().darkTheme) {
+                fill(bounds.x + 17, bounds.y + 5, bounds.x + bounds.width - 17, bounds.y + 17, 0xFF404040);
+                fill(bounds.x + 17, bounds.y + 21, bounds.x + bounds.width - 17, bounds.y + 33, 0xFF404040);
+            } else {
+                fill(bounds.x + 17, bounds.y + 5, bounds.x + bounds.width - 17, bounds.y + 17, 0xFF9E9E9E);
+                fill(bounds.x + 17, bounds.y + 21, bounds.x + bounds.width - 17, bounds.y + 33, 0xFF9E9E9E);
+            }
         }
         tabs.stream().filter(tabWidget -> !tabWidget.isSelected()).forEach(tabWidget -> tabWidget.render(mouseX, mouseY, delta));
         GuiLighting.disable();

+ 131 - 1
src/main/java/me/shedaniel/rei/gui/VillagerRecipeViewingScreen.java

@@ -10,6 +10,7 @@ import com.google.common.collect.Maps;
 import com.mojang.blaze3d.platform.GlStateManager;
 import com.zeitheron.hammercore.client.utils.Scissors;
 import me.shedaniel.cloth.api.ClientUtils;
+import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.*;
 import me.shedaniel.rei.client.ScreenHelper;
 import me.shedaniel.rei.gui.renderables.RecipeRenderer;
@@ -18,15 +19,20 @@ import net.minecraft.ChatFormat;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.Element;
 import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.render.BufferBuilder;
 import net.minecraft.client.render.GuiLighting;
+import net.minecraft.client.render.Tessellator;
+import net.minecraft.client.render.VertexFormats;
 import net.minecraft.client.resource.language.I18n;
 import net.minecraft.client.sound.PositionedSoundInstance;
+import net.minecraft.item.ItemStack;
 import net.minecraft.network.chat.TextComponent;
 import net.minecraft.network.chat.TranslatableComponent;
 import net.minecraft.sound.SoundEvents;
 import net.minecraft.util.math.MathHelper;
 
 import java.awt.*;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
@@ -46,6 +52,10 @@ public class VillagerRecipeViewingScreen extends Screen {
     public Rectangle bounds, scrollListBounds;
     private int selectedCategoryIndex, selectedRecipeIndex;
     private double scroll;
+    private float scrollBarAlpha = 0;
+    private float scrollBarAlphaFuture = 0;
+    private long scrollBarAlphaFutureTime = -1;
+    private boolean draggingScrollBar = false;
     private int tabsPage;
     
     public VillagerRecipeViewingScreen(Map<RecipeCategory, List<RecipeDisplay>> map) {
@@ -54,7 +64,10 @@ public class VillagerRecipeViewingScreen extends Screen {
         this.categoryMap = Maps.newLinkedHashMap();
         this.selectedCategoryIndex = 0;
         this.selectedRecipeIndex = 0;
+        this.scrollBarAlpha = 0;
+        this.scrollBarAlphaFuture = 0;
         this.scroll = 0;
+        this.draggingScrollBar = false;
         this.tabsPage = 0;
         this.categories = Lists.newArrayList();
         this.buttonWidgets = Lists.newArrayList();
@@ -71,6 +84,7 @@ public class VillagerRecipeViewingScreen extends Screen {
     @Override
     protected void init() {
         super.init();
+        this.draggingScrollBar = false;
         this.children.clear();
         this.widgets.clear();
         this.buttonWidgets.clear();
@@ -83,6 +97,34 @@ public class VillagerRecipeViewingScreen extends Screen {
         int guiWidth = MathHelper.clamp(category.getDisplayWidth(display) + 30, 0, largestWidth) + 100;
         int guiHeight = MathHelper.clamp(category.getDisplayHeight() + 40, 166, largestHeight);
         this.bounds = new Rectangle(width / 2 - guiWidth / 2, height / 2 - guiHeight / 2, guiWidth, guiHeight);
+        
+        List<List<ItemStack>> workingStations = RoughlyEnoughItemsCore.getRecipeHelper().getWorkingStations(category.getIdentifier());
+        if (!workingStations.isEmpty()) {
+            int ww = MathHelper.floor((bounds.width - 16) / 18f);
+            int w = Math.min(ww, workingStations.size());
+            int h = MathHelper.ceil(workingStations.size() / ((float)ww));
+            int xx = bounds.x + 16;
+            int yy = bounds.y + bounds.height + 5;
+            widgets.add(new CategoryBaseWidget(new Rectangle(xx - 6, bounds.y + bounds.height - 5, 11 + w * 18, 15 + h * 18)));
+            int index = 0;
+            List list = Collections.singletonList(ChatFormat.YELLOW.toString() + I18n.translate("text.rei.working_station"));
+            for(List<ItemStack> workingStation : workingStations) {
+                widgets.add(new SlotWidget(xx, yy, workingStation, true, true, true) {
+                    @Override
+                    protected List<String> getExtraToolTips(ItemStack stack) {
+                        return list;
+                    }
+                });
+                index++;
+                xx += 18;
+                if (index >= ww) {
+                    index = 0;
+                    xx = bounds.x + 16;
+                    yy += 18;
+                }
+            }
+        }
+        
         this.widgets.add(new CategoryBaseWidget(bounds));
         this.scrollListBounds = new Rectangle(bounds.x + 4, bounds.y + 17, 97 + 5, guiHeight - 17 - 7);
         this.widgets.add(new SlotBaseWidget(scrollListBounds));
@@ -183,9 +225,10 @@ public class VillagerRecipeViewingScreen extends Screen {
             
             @Override
             public int getDefaultColor() {
-                return 4210752;
+                return RoughlyEnoughItemsCore.getConfigManager().getConfig().darkTheme ? 0xFFBBBBBB : 4210752;
             }
         });
+        
         this.children.addAll(buttonWidgets);
         this.widgets.addAll(tabs);
         this.children.addAll(widgets);
@@ -193,6 +236,22 @@ public class VillagerRecipeViewingScreen extends Screen {
         ScreenHelper.getLastOverlay().init();
     }
     
+    @Override
+    public boolean mouseClicked(double mouseX, double mouseY, int int_1) {
+        double height = buttonWidgets.stream().map(ButtonWidget::getBounds).collect(Collectors.summingDouble(Rectangle::getHeight));
+        int actualHeight = scrollListBounds.height - 2;
+        if (height > actualHeight && scrollBarAlpha > 0 && mouseY >= scrollListBounds.y + 1 && mouseY <= scrollListBounds.getMaxY() - 1) {
+            double scrollbarPositionMinX = scrollListBounds.getMaxX() - 6;
+            if (mouseX >= scrollbarPositionMinX & mouseX <= scrollbarPositionMinX + 8) {
+                this.draggingScrollBar = true;
+                scrollBarAlpha = 1;
+                return false;
+            }
+        }
+        this.draggingScrollBar = false;
+        return super.mouseClicked(mouseX, mouseY, int_1);
+    }
+    
     @Override
     public boolean mouseScrolled(double double_1, double double_2, double double_3) {
         double height = buttonWidgets.stream().map(ButtonWidget::getBounds).collect(Collectors.summingDouble(Rectangle::getHeight));
@@ -202,6 +261,9 @@ public class VillagerRecipeViewingScreen extends Screen {
             else
                 scroll += 16;
             scroll = MathHelper.clamp(scroll, 0, height - scrollListBounds.height + 2);
+            if (scrollBarAlphaFuture == 0)
+                scrollBarAlphaFuture = 1f;
+            scrollBarAlphaFutureTime = System.currentTimeMillis();
             return true;
         }
         for(Element listener : children())
@@ -226,6 +288,28 @@ public class VillagerRecipeViewingScreen extends Screen {
     
     @Override
     public void render(int mouseX, int mouseY, float delta) {
+        if (RoughlyEnoughItemsCore.getConfigManager().getConfig().villagerScreenPermanentScrollBar) {
+            scrollBarAlphaFutureTime = System.currentTimeMillis();
+            scrollBarAlphaFuture = 0;
+            scrollBarAlpha = 1;
+        } else if (scrollBarAlphaFutureTime > 0) {
+            long l = System.currentTimeMillis() - scrollBarAlphaFutureTime;
+            if (l > 300f) {
+                if (scrollBarAlphaFutureTime == 0) {
+                    scrollBarAlpha = scrollBarAlphaFuture;
+                    scrollBarAlphaFutureTime = -1;
+                } else if (l > 2000f && scrollBarAlphaFuture == 1) {
+                    scrollBarAlphaFuture = 0;
+                    scrollBarAlphaFutureTime = System.currentTimeMillis();
+                } else
+                    scrollBarAlpha = scrollBarAlphaFuture;
+            } else {
+                if (scrollBarAlphaFuture == 0)
+                    scrollBarAlpha = Math.min(scrollBarAlpha, 1 - Math.min(1f, l / 300f));
+                else if (scrollBarAlphaFuture == 1)
+                    scrollBarAlpha = Math.max(Math.min(1f, l / 300f), scrollBarAlpha);
+            }
+        }
         this.fillGradient(0, 0, this.width, this.height, -1072689136, -804253680);
         int yOffset = 0;
         this.widgets.forEach(widget -> {
@@ -253,11 +337,57 @@ public class VillagerRecipeViewingScreen extends Screen {
                 recipeRenderers.get(i).render(buttonWidgets.get(i).getBounds().x, buttonWidgets.get(i).getBounds().y, mouseX, mouseY, delta);
             }
         }
+        double height = buttonWidgets.stream().map(ButtonWidget::getBounds).collect(Collectors.summingDouble(Rectangle::getHeight));
+        if (height > scrollListBounds.height - 2) {
+            Tessellator tessellator = Tessellator.getInstance();
+            BufferBuilder buffer = tessellator.getBufferBuilder();
+            double maxScroll = height - scrollListBounds.height + 2;
+            int scrollBarHeight = MathHelper.floor((scrollListBounds.height - 2) * (scrollListBounds.height - 2) / maxScroll);
+            scrollBarHeight = MathHelper.clamp(scrollBarHeight, 32, scrollListBounds.height - 2 - 8);
+            int minY = (int) (scroll * (scrollListBounds.height - 2 - scrollBarHeight) / maxScroll) + scrollListBounds.y + 1;
+            if (minY < this.scrollListBounds.y + 1)
+                minY = this.scrollListBounds.y + 1;
+            double scrollbarPositionMinX = scrollListBounds.getMaxX() - 6, scrollbarPositionMaxX = scrollListBounds.getMaxX() - 2;
+            GuiLighting.disable();
+            GlStateManager.disableTexture();
+            GlStateManager.enableBlend();
+            GlStateManager.disableAlphaTest();
+            GlStateManager.blendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA, GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA, GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO);
+            GlStateManager.shadeModel(7425);
+            buffer.begin(7, VertexFormats.POSITION_COLOR);
+            float b = RoughlyEnoughItemsCore.getConfigManager().getConfig().darkTheme ? 0.37f : 1f;
+            buffer.vertex(scrollbarPositionMinX, minY + scrollBarHeight, 1000D).color(b, b, b, scrollBarAlpha).next();
+            buffer.vertex(scrollbarPositionMaxX, minY + scrollBarHeight, 1000D).color(b, b, b, scrollBarAlpha).next();
+            buffer.vertex(scrollbarPositionMaxX, minY, 1000D).color(b, b, b, scrollBarAlpha).next();
+            buffer.vertex(scrollbarPositionMinX, minY, 1000D).color(b, b, b, scrollBarAlpha).next();
+            tessellator.draw();
+            GlStateManager.shadeModel(7424);
+            GlStateManager.disableBlend();
+            GlStateManager.enableAlphaTest();
+            GlStateManager.enableTexture();
+        }
         Scissors.end();
         GlStateManager.popMatrix();
         ScreenHelper.getLastOverlay().lateRender(mouseX, mouseY, delta);
     }
     
+    @Override
+    public boolean mouseDragged(double mouseX, double mouseY, int int_1, double double_3, double double_4) {
+        if (int_1 == 0 && scrollBarAlpha > 0 && draggingScrollBar) {
+            double height = buttonWidgets.stream().map(ButtonWidget::getBounds).collect(Collectors.summingDouble(Rectangle::getHeight));
+            int actualHeight = scrollListBounds.height - 2;
+            if (height > actualHeight && mouseY >= scrollListBounds.y + 1 && mouseY <= scrollListBounds.getMaxY() - 1) {
+                int int_3 = MathHelper.clamp((int) ((actualHeight * actualHeight) / height), 32, actualHeight - 8);
+                double double_6 = Math.max(1.0D, Math.max(1d, height) / (double) (actualHeight - int_3));
+                scrollBarAlphaFutureTime = System.currentTimeMillis();
+                scrollBarAlphaFuture = 1f;
+                scroll = MathHelper.clamp(scroll + double_4 * double_6, 0, height - scrollListBounds.height + 2);
+                return true;
+            }
+        }
+        return super.mouseDragged(mouseX, mouseY, int_1, double_3, double_4);
+    }
+    
     private int getReal(int i) {
         return (int) (i / ((double) minecraft.window.getScaledWidth() / (double) minecraft.window.getWidth()));
     }

+ 36 - 9
src/main/java/me/shedaniel/rei/gui/renderables/SimpleRecipeRenderer.java

@@ -15,6 +15,7 @@ import net.minecraft.util.Identifier;
 import net.minecraft.util.Pair;
 import net.minecraft.util.math.MathHelper;
 
+import java.util.Comparator;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -23,6 +24,19 @@ import java.util.stream.Collectors;
 
 public class SimpleRecipeRenderer extends RecipeRenderer {
     
+    public static final Comparator<ItemStack> ITEM_STACK_COMPARATOR = (o1, o2) -> {
+        if (o1.getItem() == o2.getItem()) {
+            if (o1.getAmount() != o2.getAmount())
+                return o1.getAmount() - o2.getAmount();
+            int compare = Boolean.compare(o1.hasTag(), o2.hasTag());
+            if (compare != 0)
+                return compare;
+            if (o1.getTag().getSize() != o2.getTag().getSize())
+                return o1.getTag().getSize() - o2.getTag().getSize();
+            return o1.getTag().hashCode() - o2.getTag().hashCode();
+        }
+        return o1.getItem().hashCode() - o2.getItem().hashCode();
+    };
     private static final Identifier CHEST_GUI_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
     private List<ItemStackRenderer> inputRenderer;
     private ItemStackRenderer outputRenderer;
@@ -31,7 +45,7 @@ public class SimpleRecipeRenderer extends RecipeRenderer {
         List<Pair<List<ItemStack>, AtomicInteger>> newList = Lists.newArrayList();
         List<Pair<List<ItemStack>, Integer>> a = input.get().stream().map(stacks -> new Pair<>(stacks, stacks.stream().map(ItemStack::getAmount).max(Integer::compareTo).orElse(1))).collect(Collectors.toList());
         for(Pair<List<ItemStack>, Integer> pair : a) {
-            Optional<Pair<List<ItemStack>, AtomicInteger>> any = newList.stream().filter(pairr -> pair.getLeft().equals(pairr.getLeft())).findAny();
+            Optional<Pair<List<ItemStack>, AtomicInteger>> any = newList.stream().filter(pairr -> equalsList(pair.getLeft(), pairr.getLeft())).findAny();
             if (any.isPresent()) {
                 any.get().getRight().addAndGet(pair.getRight());
             } else
@@ -48,9 +62,22 @@ public class SimpleRecipeRenderer extends RecipeRenderer {
         this.outputRenderer = Renderable.fromItemStacks(output.get().stream().filter(stack -> !stack.isEmpty()).collect(Collectors.toList()));
     }
     
+    public static boolean equalsList(List<ItemStack> list_1, List<ItemStack> list_2) {
+        List<ItemStack> stacks_1 = list_1.stream().distinct().sorted(ITEM_STACK_COMPARATOR).collect(Collectors.toList());
+        List<ItemStack> stacks_2 = list_2.stream().distinct().sorted(ITEM_STACK_COMPARATOR).collect(Collectors.toList());
+        if (stacks_1.equals(stacks_2))
+            return true;
+        if (stacks_1.size() != stacks_2.size())
+            return false;
+        for(int i = 0; i < stacks_1.size(); i++)
+            if (!stacks_1.get(i).isEqualIgnoreTags(stacks_2.get(i)))
+                return false;
+        return true;
+    }
+    
     @Override
     public void render(int x, int y, double mouseX, double mouseY, float delta) {
-        int xx = x + 5, yy = y + 5;
+        int xx = x + 4, yy = y + 2;
         int j = 0;
         int itemsPerLine = getItemsPerLine();
         for(ItemStackRenderer itemStackRenderer : inputRenderer) {
@@ -59,18 +86,18 @@ public class SimpleRecipeRenderer extends RecipeRenderer {
             itemStackRenderer.render(xx + 8, yy + 6, mouseX, mouseY, delta);
             xx += 18;
             j++;
-            if (j >= getItemsPerLine() - 3) {
+            if (j >= getItemsPerLine() - 2) {
                 yy += 18;
                 xx = x + 5;
                 j = 0;
             }
         }
-        xx = x + 5 + 18 * (getItemsPerLine() - 3);
+        xx = x + 5 + 18 * (getItemsPerLine() - 2);
         yy = y + getHeight() / 2 - 8;
         GuiLighting.disable();
         MinecraftClient.getInstance().getTextureManager().bindTexture(CHEST_GUI_TEXTURE);
-        blit(xx, yy, 0, 28, 36, 18);
-        xx += 36;
+        blit(xx, yy, 0, 28, 18, 18);
+        xx += 18;
         outputRenderer.setBlitOffset(getBlitOffset() + 50);
         outputRenderer.drawTooltip = MinecraftClient.getInstance().currentScreen instanceof VillagerRecipeViewingScreen;
         outputRenderer.render(xx + 8, yy + 6, mouseX, mouseY, delta);
@@ -78,15 +105,15 @@ public class SimpleRecipeRenderer extends RecipeRenderer {
     
     @Override
     public int getHeight() {
-        return 10 + getItemsHeight() * 18;
+        return 4 + getItemsHeight() * 18;
     }
     
     public int getItemsHeight() {
-        return MathHelper.ceil(((float) inputRenderer.size()) / (getItemsPerLine() - 3));
+        return MathHelper.ceil(((float) inputRenderer.size()) / (getItemsPerLine() - 2));
     }
     
     public int getItemsPerLine() {
-        return MathHelper.floor((getWidth() - 10f) / 18f);
+        return MathHelper.floor((getWidth() - 4f) / 18f);
     }
     
 }

+ 3 - 1
src/main/java/me/shedaniel/rei/gui/widget/ButtonWidget.java

@@ -6,6 +6,7 @@
 package me.shedaniel.rei.gui.widget;
 
 import com.mojang.blaze3d.platform.GlStateManager;
+import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.client.ScreenHelper;
 import net.minecraft.client.gui.Element;
 import net.minecraft.client.sound.PositionedSoundInstance;
@@ -22,6 +23,7 @@ import java.util.Optional;
 public abstract class ButtonWidget extends HighlightableWidget {
     
     public static final Identifier BUTTON_LOCATION = new Identifier("roughlyenoughitems", "textures/gui/button.png");
+    public static final Identifier BUTTON_LOCATION_DARK = new Identifier("roughlyenoughitems", "textures/gui/button_dark.png");
     public String text;
     public boolean enabled;
     public boolean focused;
@@ -63,7 +65,7 @@ public abstract class ButtonWidget extends HighlightableWidget {
     @Override
     public void render(int mouseX, int mouseY, float delta) {
         int x = bounds.x, y = bounds.y, width = bounds.width, height = bounds.height;
-        minecraft.getTextureManager().bindTexture(BUTTON_LOCATION);
+        minecraft.getTextureManager().bindTexture(RoughlyEnoughItemsCore.getConfigManager().getConfig().darkTheme ? BUTTON_LOCATION_DARK : BUTTON_LOCATION);
         GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
         int textureOffset = this.getTextureId(isHovered(mouseX, mouseY));
         GlStateManager.enableBlend();

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

@@ -5,6 +5,7 @@
 
 package me.shedaniel.rei.gui.widget;
 
+import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.client.ScreenHelper;
 import net.minecraft.ChatFormat;
 
@@ -13,7 +14,6 @@ import java.util.Optional;
 
 public abstract class ClickableLabelWidget extends LabelWidget {
     
-    public static final int hoveredColor = (new Color(102, 255, 204)).getRGB();
     public boolean focused;
     public boolean clickable;
     
@@ -40,11 +40,11 @@ public abstract class ClickableLabelWidget extends LabelWidget {
     }
     
     public int getDefaultColor() {
-        return -1;
+        return RoughlyEnoughItemsCore.getConfigManager().getConfig().darkTheme ? 0xFFBBBBBB : -1;
     }
     
     public int getHoveredColor() {
-        return hoveredColor;
+        return RoughlyEnoughItemsCore.getConfigManager().getConfig().darkTheme ? -1 : 0xFF66FFCC;
     }
     
     @Override

+ 35 - 39
src/main/java/me/shedaniel/rei/gui/widget/ItemListOverlay.java

@@ -97,6 +97,39 @@ public class ItemListOverlay extends Widget {
         return "ERROR";
     }
     
+    public static boolean filterItem(ItemStack itemStack, List<SearchArgument[]> arguments) {
+        if (arguments.isEmpty())
+            return true;
+        String mod = ClientHelper.getInstance().getModFromItem(itemStack.getItem()).toLowerCase();
+        String tooltips = tryGetItemStackToolTip(itemStack, true).stream().skip(1).collect(Collectors.joining("")).replace(SPACE, EMPTY).toLowerCase();
+        String name = tryGetItemStackName(itemStack).replace(SPACE, EMPTY).toLowerCase();
+        for(SearchArgument[] arguments1 : arguments) {
+            boolean b = true;
+            for(SearchArgument argument : arguments1) {
+                if (argument.getArgumentType().equals(SearchArgument.ArgumentType.ALWAYS))
+                    return true;
+                if (argument.getArgumentType().equals(SearchArgument.ArgumentType.MOD))
+                    if (SearchArgument.getFunction(!argument.isInclude()).apply(mod.indexOf(argument.getText()))) {
+                        b = false;
+                        break;
+                    }
+                if (argument.getArgumentType().equals(SearchArgument.ArgumentType.TOOLTIP))
+                    if (SearchArgument.getFunction(!argument.isInclude()).apply(tooltips.indexOf(argument.getText()))) {
+                        b = false;
+                        break;
+                    }
+                if (argument.getArgumentType().equals(SearchArgument.ArgumentType.TEXT))
+                    if (SearchArgument.getFunction(!argument.isInclude()).apply(name.indexOf(argument.getText()))) {
+                        b = false;
+                        break;
+                    }
+            }
+            if (b)
+                return true;
+        }
+        return false;
+    }
+    
     public int getFullTotalSlotsPerPage() {
         return width * height;
     }
@@ -242,13 +275,9 @@ public class ItemListOverlay extends Widget {
                 lastSearchArgument.add(new SearchArgument[]{SearchArgument.ALWAYS});
         });
         os.stream().filter(itemStack -> filterItem(itemStack, lastSearchArgument)).forEachOrdered(stacks::add);
-        List<ItemStack> workingItems = RoughlyEnoughItemsCore.getConfigManager().isCraftableOnlyEnabled() && !stacks.isEmpty() && !inventoryItems.isEmpty() ? Lists.newArrayList() : Lists.newArrayList(ol);
-        if (RoughlyEnoughItemsCore.getConfigManager().isCraftableOnlyEnabled()) {
-            RecipeHelper.getInstance().findCraftableByItems(inventoryItems).forEach(workingItems::add);
-            workingItems.addAll(inventoryItems);
-        }
-        if (!RoughlyEnoughItemsCore.getConfigManager().isCraftableOnlyEnabled())
+        if (!RoughlyEnoughItemsCore.getConfigManager().isCraftableOnlyEnabled() || stacks.isEmpty() || inventoryItems.isEmpty())
             return stacks;
+        List<ItemStack> workingItems = Lists.newArrayList(RecipeHelper.getInstance().findCraftableByItems(inventoryItems));
         return stacks.stream().filter(itemStack -> workingItems.stream().anyMatch(stack -> stack.isEqualIgnoreTags(itemStack))).collect(Collectors.toList());
     }
     
@@ -256,39 +285,6 @@ public class ItemListOverlay extends Widget {
         return lastSearchArgument;
     }
     
-    public static boolean filterItem(ItemStack itemStack, List<SearchArgument[]> arguments) {
-        if (arguments.isEmpty())
-            return true;
-        String mod = ClientHelper.getInstance().getModFromItem(itemStack.getItem()).toLowerCase();
-        String tooltips = tryGetItemStackToolTip(itemStack, true).stream().skip(1).collect(Collectors.joining("")).replace(SPACE, EMPTY).toLowerCase();
-        String name = tryGetItemStackName(itemStack).replace(SPACE, EMPTY).toLowerCase();
-        for(SearchArgument[] arguments1 : arguments) {
-            boolean b = true;
-            for(SearchArgument argument : arguments1) {
-                if (argument.getArgumentType().equals(SearchArgument.ArgumentType.ALWAYS))
-                    return true;
-                if (argument.getArgumentType().equals(SearchArgument.ArgumentType.MOD))
-                    if (SearchArgument.getFunction(!argument.isInclude()).apply(mod.indexOf(argument.getText()))) {
-                        b = false;
-                        break;
-                    }
-                if (argument.getArgumentType().equals(SearchArgument.ArgumentType.TOOLTIP))
-                    if (SearchArgument.getFunction(!argument.isInclude()).apply(tooltips.indexOf(argument.getText()))) {
-                        b = false;
-                        break;
-                    }
-                if (argument.getArgumentType().equals(SearchArgument.ArgumentType.TEXT))
-                    if (SearchArgument.getFunction(!argument.isInclude()).apply(name.indexOf(argument.getText()))) {
-                        b = false;
-                        break;
-                    }
-            }
-            if (b)
-                return true;
-        }
-        return false;
-    }
-    
     private boolean filterItem(ItemStack itemStack, SearchArgument... arguments) {
         if (arguments.length == 0)
             return true;

+ 4 - 0
src/main/java/me/shedaniel/rei/gui/widget/ItemSlotWidget.java

@@ -11,6 +11,10 @@ import net.minecraft.item.ItemStack;
 import java.util.Collection;
 import java.util.List;
 
+/**
+ * @deprecated Use {@link SlotWidget}
+ */
+@Deprecated
 public class ItemSlotWidget extends SlotWidget {
     public ItemSlotWidget(int x, int y, ItemStack itemStack, boolean drawBackground, boolean showToolTips) {
         super(x, y, itemStack, drawBackground, showToolTips);

+ 13 - 0
src/main/java/me/shedaniel/rei/gui/widget/QueuedTooltip.java

@@ -12,11 +12,13 @@ import me.shedaniel.cloth.api.ClientUtils;
 import java.awt.*;
 import java.util.Collections;
 import java.util.List;
+import java.util.function.Consumer;
 
 public class QueuedTooltip {
     
     private Point location;
     private List<String> text;
+    private Consumer<QueuedTooltip> consumer = null;
     
     private QueuedTooltip(Point location, List<String> text) {
         this.location = location;
@@ -39,6 +41,17 @@ public class QueuedTooltip {
         return QueuedTooltip.create(ClientUtils.getMouseLocation(), text);
     }
     
+    @Deprecated
+    public QueuedTooltip setSpecialRenderer(Consumer<QueuedTooltip> consumer) {
+        this.consumer = consumer;
+        return this;
+    }
+    
+    @Deprecated
+    public Consumer<QueuedTooltip> getConsumer() {
+        return consumer;
+    }
+    
     public Point getLocation() {
         return location;
     }

+ 11 - 2
src/main/java/me/shedaniel/rei/gui/widget/RecipeBaseWidget.java

@@ -18,6 +18,7 @@ import java.util.List;
 public class RecipeBaseWidget extends HighlightableWidget {
     
     private static final Identifier CHEST_GUI_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
+    private static final Identifier CHEST_GUI_TEXTURE_DARK = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer_dark.png");
     
     private Rectangle bounds;
     
@@ -27,6 +28,14 @@ public class RecipeBaseWidget extends HighlightableWidget {
             throw new IllegalArgumentException("Base too small, at least 8x8!");
     }
     
+    public int getBlitOffset() {
+        return this.blitOffset;
+    }
+    
+    public void setBlitOffset(int offset) {
+        this.blitOffset = offset;
+    }
+    
     @Override
     public Rectangle getBounds() {
         return bounds;
@@ -47,7 +56,7 @@ public class RecipeBaseWidget extends HighlightableWidget {
             return;
         GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
         GuiLighting.disable();
-        minecraft.getTextureManager().bindTexture(CHEST_GUI_TEXTURE);
+        minecraft.getTextureManager().bindTexture(RoughlyEnoughItemsCore.getConfigManager().getConfig().darkTheme ? CHEST_GUI_TEXTURE_DARK : CHEST_GUI_TEXTURE);
         int x = bounds.x, y = bounds.y, width = bounds.width, height = bounds.height;
         int textureOffset = getTextureOffset();
         
@@ -76,7 +85,7 @@ public class RecipeBaseWidget extends HighlightableWidget {
     }
     
     protected int getInnerColor() {
-        return -3750202;
+        return RoughlyEnoughItemsCore.getConfigManager().getConfig().darkTheme ? 0xFF2E2E2E : -3750202;
     }
     
     protected int getTextureOffset() {

+ 3 - 1
src/main/java/me/shedaniel/rei/gui/widget/SlotBaseWidget.java

@@ -5,6 +5,8 @@
 
 package me.shedaniel.rei.gui.widget;
 
+import me.shedaniel.rei.RoughlyEnoughItemsCore;
+
 import java.awt.*;
 
 public class SlotBaseWidget extends RecipeBaseWidget {
@@ -15,7 +17,7 @@ public class SlotBaseWidget extends RecipeBaseWidget {
     
     @Override
     public int getInnerColor() {
-        return -7631989;
+        return RoughlyEnoughItemsCore.getConfigManager().getConfig().darkTheme ? 0xFF303030 : -7631989;
     }
     
     @Override

+ 31 - 8
src/main/java/me/shedaniel/rei/gui/widget/SlotWidget.java

@@ -17,7 +17,6 @@ import me.shedaniel.rei.client.ScreenHelper;
 import me.shedaniel.rei.gui.renderables.ItemStackRenderer;
 import net.minecraft.client.gui.Element;
 import net.minecraft.item.ItemStack;
-import net.minecraft.item.Items;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.math.MathHelper;
 
@@ -30,11 +29,11 @@ import java.util.stream.Collectors;
 
 public class SlotWidget extends HighlightableWidget {
     
-    private static final Identifier RECIPE_GUI = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
-    private static final ItemStackRenderer TROPICAL_FISH_RENDERABLE = Renderable.fromItemStack(Items.TROPICAL_FISH.getDefaultStack());
+    public static final Identifier RECIPE_GUI = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
+    public static final Identifier RECIPE_GUI_DARK = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer_dark.png");
     private List<Renderer> renderers = new LinkedList<>();
     private boolean drawBackground, showToolTips, clickToMoreRecipes, drawHighlightedBackground;
-    private int x, y;
+    protected int x, y;
     
     public SlotWidget(int x, int y, ItemStack itemStack, boolean drawBackground, boolean showToolTips) {
         this(x, y, Collections.singletonList(itemStack), drawBackground, showToolTips);
@@ -59,6 +58,22 @@ public class SlotWidget extends HighlightableWidget {
         this.clickToMoreRecipes = clickToMoreRecipes;
     }
     
+    public int getX() {
+        return x;
+    }
+    
+    public void setX(int x) {
+        this.x = x;
+    }
+    
+    public int getY() {
+        return y;
+    }
+    
+    public void setY(int y) {
+        this.y = y;
+    }
+    
     public boolean isShowToolTips() {
         return showToolTips;
     }
@@ -99,8 +114,9 @@ public class SlotWidget extends HighlightableWidget {
     @Override
     public void render(int mouseX, int mouseY, float delta) {
         Renderer renderer = getCurrentRenderer();
+        boolean darkTheme = RoughlyEnoughItemsCore.getConfigManager().getConfig().darkTheme;
         if (drawBackground) {
-            minecraft.getTextureManager().bindTexture(RECIPE_GUI);
+            minecraft.getTextureManager().bindTexture(darkTheme ? RECIPE_GUI_DARK : RECIPE_GUI);
             blit(this.x - 1, this.y - 1, 0, 222, 18, 18);
         }
         boolean highlighted = isHighlighted(mouseX, mouseY);
@@ -108,14 +124,13 @@ public class SlotWidget extends HighlightableWidget {
             GlStateManager.disableLighting();
             GlStateManager.disableDepthTest();
             GlStateManager.colorMask(true, true, true, false);
-            fillGradient(x, y, x + 16, y + 16, -2130706433, -2130706433);
+            int color = darkTheme ? 0xFF5E5E5E : -2130706433;
+            fillGradient(x, y, x + 16, y + 16, color, color);
             GlStateManager.colorMask(true, true, true, true);
             GlStateManager.enableLighting();
             GlStateManager.enableDepthTest();
         }
         if (isCurrentRendererItem() && !getCurrentItemStack().isEmpty()) {
-            if (RoughlyEnoughItemsCore.getConfigManager().getConfig().aprilFoolsFish2019 && !highlighted)
-                renderer = TROPICAL_FISH_RENDERABLE;
             renderer.setBlitOffset(200);
             renderer.render(x + 8, y + 6, mouseX, mouseY, delta);
             if (!getCurrentItemStack().isEmpty() && highlighted && showToolTips)
@@ -126,6 +141,14 @@ public class SlotWidget extends HighlightableWidget {
         }
     }
     
+    public int getBlitOffset() {
+        return this.blitOffset;
+    }
+    
+    public void setBlitOffset(int offset) {
+        this.blitOffset = offset;
+    }
+    
     protected void queueTooltip(ItemStack itemStack, float delta) {
         ScreenHelper.getLastOverlay().addTooltip(QueuedTooltip.create(getTooltip(itemStack)));
     }

+ 10 - 0
src/main/java/me/shedaniel/rei/gui/widget/SpeedCraftingButtonWidget.java

@@ -8,7 +8,9 @@ package me.shedaniel.rei.gui.widget;
 import me.shedaniel.rei.api.RecipeDisplay;
 import me.shedaniel.rei.api.SpeedCraftFunctional;
 import me.shedaniel.rei.client.ScreenHelper;
+import net.minecraft.ChatFormat;
 import net.minecraft.client.resource.language.I18n;
+import net.minecraft.recipe.Recipe;
 
 import java.awt.*;
 import java.util.Optional;
@@ -18,11 +20,14 @@ public class SpeedCraftingButtonWidget extends ButtonWidget {
     
     private final Supplier<RecipeDisplay> displaySupplier;
     private final SpeedCraftFunctional functional;
+    private String extraTooltip;
     
     public SpeedCraftingButtonWidget(Rectangle rectangle, String text, SpeedCraftFunctional functional, Supplier<RecipeDisplay> displaySupplier) {
         super(rectangle, text);
         this.displaySupplier = displaySupplier;
         this.functional = functional;
+        Optional<Recipe> recipe = displaySupplier.get().getRecipe();
+        extraTooltip = recipe.isPresent() ? I18n.translate("text.rei.recipe_id", ChatFormat.GRAY.toString(), recipe.get().getId().toString()) : "";
     }
     
     @Override
@@ -40,6 +45,11 @@ public class SpeedCraftingButtonWidget extends ButtonWidget {
     
     @Override
     public Optional<String> getTooltips() {
+        if (this.minecraft.options.advancedItemTooltips)
+            if (enabled)
+                return Optional.ofNullable(I18n.translate("text.speed_craft.move_items") + extraTooltip);
+            else
+                return Optional.ofNullable(I18n.translate("text.speed_craft.failed_move_items") + extraTooltip);
         if (enabled)
             return Optional.ofNullable(I18n.translate("text.speed_craft.move_items"));
         else

+ 6 - 2
src/main/java/me/shedaniel/rei/gui/widget/TabWidget.java

@@ -6,6 +6,7 @@
 package me.shedaniel.rei.gui.widget;
 
 import com.mojang.blaze3d.platform.GlStateManager;
+import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.ClientHelper;
 import me.shedaniel.rei.api.RecipeCategory;
 import me.shedaniel.rei.api.Renderer;
@@ -21,6 +22,7 @@ import java.util.List;
 public class TabWidget extends HighlightableWidget {
     
     public static final Identifier CHEST_GUI_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
+    public static final Identifier CHEST_GUI_TEXTURE_DARK = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer_dark.png");
     
     public boolean shown = false, selected = false;
     public Renderer renderer;
@@ -73,15 +75,17 @@ public class TabWidget extends HighlightableWidget {
         if (shown) {
             GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
             GuiLighting.disable();
-            minecraft.getTextureManager().bindTexture(CHEST_GUI_TEXTURE);
+            minecraft.getTextureManager().bindTexture(RoughlyEnoughItemsCore.getConfigManager().getConfig().darkTheme ? CHEST_GUI_TEXTURE_DARK : CHEST_GUI_TEXTURE);
             this.blit(bounds.x, bounds.y + 2, selected ? 28 : 0, 192, 28, (selected ? 30 : 27));
             renderer.setBlitOffset(100);
             renderer.render((int) bounds.getCenterX(), (int) bounds.getCenterY(), mouseX, mouseY, delta);
-            if (isHighlighted(mouseX, mouseY))
+            if (isHighlighted(mouseX, mouseY)) {
                 drawTooltip();
+            }
         }
     }
     
+    @SuppressWarnings("deprecation")
     private void drawTooltip() {
         if (this.minecraft.options.advancedItemTooltips)
             ScreenHelper.getLastOverlay().addTooltip(QueuedTooltip.create(categoryName, ChatFormat.DARK_GRAY.toString() + category.getIdentifier().toString(), ClientHelper.getInstance().getFormattedModFromIdentifier(category.getIdentifier())));

+ 2 - 3
src/main/java/me/shedaniel/rei/plugin/DefaultBlastingCategory.java

@@ -6,6 +6,7 @@
 package me.shedaniel.rei.plugin;
 
 import com.mojang.blaze3d.platform.GlStateManager;
+import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.RecipeCategory;
 import me.shedaniel.rei.api.Renderable;
 import me.shedaniel.rei.api.Renderer;
@@ -31,8 +32,6 @@ import java.util.function.Supplier;
 
 public class DefaultBlastingCategory implements RecipeCategory<DefaultBlastingDisplay> {
     
-    private static final Identifier DISPLAY_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/display.png");
-    
     @Override
     public Identifier getIdentifier() {
         return DefaultPlugin.BLASTING;
@@ -63,7 +62,7 @@ public class DefaultBlastingCategory implements RecipeCategory<DefaultBlastingDi
                 super.render(mouseX, mouseY, delta);
                 GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
                 GuiLighting.disable();
-                MinecraftClient.getInstance().getTextureManager().bindTexture(DISPLAY_TEXTURE);
+                MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
                 blit(startPoint.x, startPoint.y, 0, 54, 82, 54);
                 int height = MathHelper.ceil((System.currentTimeMillis() / 250 % 14d) / 1f);
                 blit(startPoint.x + 2, startPoint.y + 21 + (14 - height), 82, 77 + (14 - height), 14, height);

+ 1 - 3
src/main/java/me/shedaniel/rei/plugin/DefaultBrewingCategory.java

@@ -31,8 +31,6 @@ import java.util.function.Supplier;
 
 public class DefaultBrewingCategory implements RecipeCategory<DefaultBrewingDisplay> {
     
-    private static final Identifier DISPLAY_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/display.png");
-    
     @Override
     public Identifier getIdentifier() {
         return DefaultPlugin.BREWING;
@@ -58,7 +56,7 @@ public class DefaultBrewingCategory implements RecipeCategory<DefaultBrewingDisp
                 super.render(mouseX, mouseY, delta);
                 GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
                 GuiLighting.disable();
-                MinecraftClient.getInstance().getTextureManager().bindTexture(DISPLAY_TEXTURE);
+                MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
                 blit(startPoint.x, startPoint.y, 0, 108, 103, 59);
                 int width = MathHelper.ceil((System.currentTimeMillis() / 250 % 18d) / 1f);
                 blit(startPoint.x + 44, startPoint.y + 28, 103, 163, width, 4);

+ 1 - 3
src/main/java/me/shedaniel/rei/plugin/DefaultCampfireCategory.java

@@ -28,8 +28,6 @@ import java.util.function.Supplier;
 
 public class DefaultCampfireCategory implements RecipeCategory<DefaultCampfireDisplay> {
     
-    private static final Identifier DISPLAY_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/display.png");
-    
     @Override
     public Identifier getIdentifier() {
         return DefaultPlugin.CAMPFIRE;
@@ -54,7 +52,7 @@ public class DefaultCampfireCategory implements RecipeCategory<DefaultCampfireDi
                 super.render(mouseX, mouseY, delta);
                 GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
                 GuiLighting.disable();
-                MinecraftClient.getInstance().getTextureManager().bindTexture(DISPLAY_TEXTURE);
+                MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
                 blit(startPoint.x, startPoint.y, 0, 167, 82, 54);
                 int height = MathHelper.ceil((System.currentTimeMillis() / 250 % 14d) / 1f);
                 blit(startPoint.x + 2, startPoint.y + 31 + (14 - height), 82, 77 + (14 - height), 14, height);

+ 1 - 3
src/main/java/me/shedaniel/rei/plugin/DefaultCraftingCategory.java

@@ -29,8 +29,6 @@ import java.util.function.Supplier;
 
 public class DefaultCraftingCategory implements RecipeCategory<DefaultCraftingDisplay> {
     
-    private static final Identifier DISPLAY_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/display.png");
-    
     @Override
     public Identifier getIdentifier() {
         return DefaultPlugin.CRAFTING;
@@ -55,7 +53,7 @@ public class DefaultCraftingCategory implements RecipeCategory<DefaultCraftingDi
                 super.render(mouseX, mouseY, delta);
                 GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
                 GuiLighting.disable();
-                MinecraftClient.getInstance().getTextureManager().bindTexture(DISPLAY_TEXTURE);
+                MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
                 blit(startPoint.x, startPoint.y, 0, 0, 116, 54);
             }
         }));

+ 25 - 24
src/main/java/me/shedaniel/rei/plugin/DefaultPlugin.java

@@ -42,9 +42,14 @@ public class DefaultPlugin implements REIPluginEntry {
     public static final Identifier STONE_CUTTING = new Identifier("minecraft", "plugins/stone_cutting");
     public static final Identifier BREWING = new Identifier("minecraft", "plugins/brewing");
     public static final Identifier PLUGIN = new Identifier("roughlyenoughitems", "default_plugin");
-    
+    private static final Identifier DISPLAY_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/display.png");
+    private static final Identifier DISPLAY_TEXTURE_DARK = new Identifier("roughlyenoughitems", "textures/gui/display_dark.png");
     private static final List<DefaultBrewingDisplay> BREWING_DISPLAYS = Lists.newArrayList();
     
+    public static Identifier getDisplayTexture() {
+        return RoughlyEnoughItemsCore.getConfigManager().getConfig().darkTheme ? DISPLAY_TEXTURE_DARK : DISPLAY_TEXTURE;
+    }
+    
     public static void registerBrewingDisplay(DefaultBrewingDisplay display) {
         BREWING_DISPLAYS.add(display);
     }
@@ -97,21 +102,13 @@ public class DefaultPlugin implements REIPluginEntry {
     
     @Override
     public void registerRecipeDisplays(RecipeHelper recipeHelper) {
-        for(Recipe recipe : recipeHelper.getAllSortedRecipes())
-            if (recipe instanceof ShapelessRecipe)
-                recipeHelper.registerDisplay(CRAFTING, new DefaultShapelessDisplay((ShapelessRecipe) recipe));
-            else if (recipe instanceof ShapedRecipe)
-                recipeHelper.registerDisplay(CRAFTING, new DefaultShapedDisplay((ShapedRecipe) recipe));
-            else if (recipe instanceof SmeltingRecipe)
-                recipeHelper.registerDisplay(SMELTING, new DefaultSmeltingDisplay((SmeltingRecipe) recipe));
-            else if (recipe instanceof SmokingRecipe)
-                recipeHelper.registerDisplay(SMOKING, new DefaultSmokingDisplay((SmokingRecipe) recipe));
-            else if (recipe instanceof BlastingRecipe)
-                recipeHelper.registerDisplay(BLASTING, new DefaultBlastingDisplay((BlastingRecipe) recipe));
-            else if (recipe instanceof CampfireCookingRecipe)
-                recipeHelper.registerDisplay(CAMPFIRE, new DefaultCampfireDisplay((CampfireCookingRecipe) recipe));
-            else if (recipe instanceof StonecuttingRecipe)
-                recipeHelper.registerDisplay(STONE_CUTTING, new DefaultStoneCuttingDisplay((StonecuttingRecipe) recipe));
+        recipeHelper.registerRecipes(CRAFTING, ShapelessRecipe.class, DefaultShapelessDisplay::new);
+        recipeHelper.registerRecipes(CRAFTING, ShapedRecipe.class, DefaultShapedDisplay::new);
+        recipeHelper.registerRecipes(SMELTING, SmeltingRecipe.class, DefaultSmeltingDisplay::new);
+        recipeHelper.registerRecipes(SMOKING, SmokingRecipe.class, DefaultSmokingDisplay::new);
+        recipeHelper.registerRecipes(BLASTING, BlastingRecipe.class, DefaultBlastingDisplay::new);
+        recipeHelper.registerRecipes(CAMPFIRE, CampfireCookingRecipe.class, DefaultCampfireDisplay::new);
+        recipeHelper.registerRecipes(STONE_CUTTING, StonecuttingRecipe.class, DefaultStoneCuttingDisplay::new);
         BREWING_DISPLAYS.stream().forEachOrdered(display -> recipeHelper.registerDisplay(BREWING, display));
         List<ItemStack> arrowStack = Collections.singletonList(Items.ARROW.getDefaultStack());
         RoughlyEnoughItemsCore.getItemRegisterer().getItemList().stream().filter(stack -> stack.getItem().equals(Items.LINGERING_POTION)).forEach(stack -> {
@@ -238,6 +235,13 @@ public class DefaultPlugin implements REIPluginEntry {
     
     @Override
     public void registerOthers(RecipeHelper recipeHelper) {
+        recipeHelper.registerWorkingStations(CRAFTING, new ItemStack(Items.CRAFTING_TABLE));
+        recipeHelper.registerWorkingStations(SMELTING, new ItemStack(Items.FURNACE));
+        recipeHelper.registerWorkingStations(SMOKING, new ItemStack(Items.SMOKER));
+        recipeHelper.registerWorkingStations(BLASTING, new ItemStack(Items.BLAST_FURNACE));
+        recipeHelper.registerWorkingStations(CAMPFIRE, new ItemStack(Items.CAMPFIRE));
+        recipeHelper.registerWorkingStations(BREWING, new ItemStack(Items.BREWING_STAND));
+        recipeHelper.registerWorkingStations(STONE_CUTTING, new ItemStack(Items.STONECUTTER));
         recipeHelper.registerRecipeVisibilityHandler(new DisplayVisibilityHandler() {
             @Override
             public DisplayVisibility handleDisplay(RecipeCategory category, RecipeDisplay display) {
@@ -249,10 +253,7 @@ public class DefaultPlugin implements REIPluginEntry {
                 return -1f;
             }
         });
-        recipeHelper.registerDefaultSpeedCraftButtonArea(DefaultPlugin.CRAFTING);
-        recipeHelper.registerDefaultSpeedCraftButtonArea(DefaultPlugin.SMELTING);
-        recipeHelper.registerDefaultSpeedCraftButtonArea(DefaultPlugin.SMOKING);
-        recipeHelper.registerDefaultSpeedCraftButtonArea(DefaultPlugin.BLASTING);
+        recipeHelper.registerSpeedCraftButtonArea(DefaultPlugin.CAMPFIRE, bounds -> new Rectangle((int) bounds.getMaxX() - 16, bounds.y + 6, 10, 10));
         recipeHelper.registerSpeedCraftFunctional(DefaultPlugin.CRAFTING, new SpeedCraftFunctional<DefaultCraftingDisplay>() {
             @Override
             public Class[] getFunctioningFor() {
@@ -269,7 +270,7 @@ public class DefaultPlugin implements REIPluginEntry {
                     ((RecipeBookGuiHooks) (((InventoryScreen) screen).getRecipeBookGui())).rei_getGhostSlots().reset();
                 else
                     return false;
-                MinecraftClient.getInstance().interactionManager.clickRecipe(MinecraftClient.getInstance().player.container.syncId, (Recipe) recipe.getRecipe().get(), Screen.hasShiftDown());
+                MinecraftClient.getInstance().interactionManager.clickRecipe(MinecraftClient.getInstance().player.container.syncId, (Recipe<?>) recipe.getRecipe().get(), Screen.hasShiftDown());
                 return true;
             }
             
@@ -292,7 +293,7 @@ public class DefaultPlugin implements REIPluginEntry {
                     ((RecipeBookGuiHooks) (((FurnaceScreen) screen).getRecipeBookGui())).rei_getGhostSlots().reset();
                 else
                     return false;
-                MinecraftClient.getInstance().interactionManager.clickRecipe(MinecraftClient.getInstance().player.container.syncId, (Recipe) recipe.getRecipe().get(), Screen.hasShiftDown());
+                MinecraftClient.getInstance().interactionManager.clickRecipe(MinecraftClient.getInstance().player.container.syncId, (Recipe<?>) recipe.getRecipe().get(), Screen.hasShiftDown());
                 return true;
             }
             
@@ -315,7 +316,7 @@ public class DefaultPlugin implements REIPluginEntry {
                     ((RecipeBookGuiHooks) (((SmokerScreen) screen).getRecipeBookGui())).rei_getGhostSlots().reset();
                 else
                     return false;
-                MinecraftClient.getInstance().interactionManager.clickRecipe(MinecraftClient.getInstance().player.container.syncId, (Recipe) recipe.getRecipe().get(), Screen.hasShiftDown());
+                MinecraftClient.getInstance().interactionManager.clickRecipe(MinecraftClient.getInstance().player.container.syncId, (Recipe<?>) recipe.getRecipe().get(), Screen.hasShiftDown());
                 return true;
             }
             
@@ -343,7 +344,7 @@ public class DefaultPlugin implements REIPluginEntry {
                     ((RecipeBookGuiHooks) (((BlastFurnaceScreen) screen).getRecipeBookGui())).rei_getGhostSlots().reset();
                 else
                     return false;
-                MinecraftClient.getInstance().interactionManager.clickRecipe(MinecraftClient.getInstance().player.container.syncId, (Recipe) recipe.getRecipe().get(), Screen.hasShiftDown());
+                MinecraftClient.getInstance().interactionManager.clickRecipe(MinecraftClient.getInstance().player.container.syncId, (Recipe<?>) recipe.getRecipe().get(), Screen.hasShiftDown());
                 return true;
             }
         });

+ 1 - 3
src/main/java/me/shedaniel/rei/plugin/DefaultSmeltingCategory.java

@@ -31,8 +31,6 @@ import java.util.function.Supplier;
 
 public class DefaultSmeltingCategory implements RecipeCategory<DefaultSmeltingDisplay> {
     
-    private static final Identifier DISPLAY_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/display.png");
-    
     @Override
     public Identifier getIdentifier() {
         return DefaultPlugin.SMELTING;
@@ -62,7 +60,7 @@ public class DefaultSmeltingCategory implements RecipeCategory<DefaultSmeltingDi
                 super.render(mouseX, mouseY, delta);
                 GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
                 GuiLighting.disable();
-                MinecraftClient.getInstance().getTextureManager().bindTexture(DISPLAY_TEXTURE);
+                MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
                 blit(startPoint.x, startPoint.y, 0, 54, 82, 54);
                 int height = MathHelper.ceil((System.currentTimeMillis() / 250 % 14d) / 1f);
                 blit(startPoint.x + 2, startPoint.y + 21 + (14 - height), 82, 77 + (14 - height), 14, height);

+ 1 - 3
src/main/java/me/shedaniel/rei/plugin/DefaultSmokingCategory.java

@@ -31,8 +31,6 @@ import java.util.function.Supplier;
 
 public class DefaultSmokingCategory implements RecipeCategory<DefaultSmokingDisplay> {
     
-    private static final Identifier DISPLAY_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/display.png");
-    
     @Override
     public Identifier getIdentifier() {
         return DefaultPlugin.SMOKING;
@@ -62,7 +60,7 @@ public class DefaultSmokingCategory implements RecipeCategory<DefaultSmokingDisp
                 super.render(mouseX, mouseY, delta);
                 GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
                 GuiLighting.disable();
-                MinecraftClient.getInstance().getTextureManager().bindTexture(DISPLAY_TEXTURE);
+                MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
                 blit(startPoint.x, startPoint.y, 0, 54, 82, 54);
                 int height = MathHelper.ceil((System.currentTimeMillis() / 250 % 14d) / 1f);
                 blit(startPoint.x + 2, startPoint.y + 21 + (14 - height), 82, 77 + (14 - height), 14, height);

+ 1 - 3
src/main/java/me/shedaniel/rei/plugin/DefaultStoneCuttingCategory.java

@@ -28,8 +28,6 @@ import java.util.function.Supplier;
 
 public class DefaultStoneCuttingCategory implements RecipeCategory<DefaultStoneCuttingDisplay> {
     
-    private static final Identifier DISPLAY_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/display.png");
-    
     @Override
     public Identifier getIdentifier() {
         return DefaultPlugin.STONE_CUTTING;
@@ -54,7 +52,7 @@ public class DefaultStoneCuttingCategory implements RecipeCategory<DefaultStoneC
                 super.render(mouseX, mouseY, delta);
                 GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
                 GuiLighting.disable();
-                MinecraftClient.getInstance().getTextureManager().bindTexture(DISPLAY_TEXTURE);
+                MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
                 blit(startPoint.x, startPoint.y, 0, 221, 82, 26);
             }
         }));

+ 3 - 3
src/main/java/me/shedaniel/rei/utils/ClothScreenRegistry.java

@@ -52,6 +52,7 @@ public class ClothScreenRegistry {
             }
         });
         ConfigScreenBuilder.CategoryBuilder appearance = builder.addCategory("text.rei.config.appearance");
+        appearance.addOption(new BooleanListEntry("text.rei.config.dark_theme", RoughlyEnoughItemsCore.getConfigManager().getConfig().darkTheme, RESET, () -> false, bool -> RoughlyEnoughItemsCore.getConfigManager().getConfig().darkTheme = bool, () -> getConfigTooltip("dark_theme")));
         appearance.addOption(new EnumListEntry<>("text.rei.config.recipe_screen_type", RecipeScreenType.class, RoughlyEnoughItemsCore.getConfigManager().getConfig().screenType, RESET, () -> RecipeScreenType.UNSET, bool -> RoughlyEnoughItemsCore.getConfigManager().getConfig().screenType = bool, EnumListEntry.DEFAULT_NAME_PROVIDER, () -> getConfigTooltip("recipe_screen_type")));
         appearance.addOption(new BooleanListEntry("text.rei.config.side_search_box", RoughlyEnoughItemsCore.getConfigManager().getConfig().sideSearchField, RESET, () -> false, bool -> RoughlyEnoughItemsCore.getConfigManager().getConfig().sideSearchField = bool, () -> getConfigTooltip("side_search_box")));
         appearance.addOption(new EnumListEntry<>("text.rei.config.list_ordering", ItemListOrderingConfig.class, ItemListOrderingConfig.from(RoughlyEnoughItemsCore.getConfigManager().getConfig().itemListOrdering, RoughlyEnoughItemsCore.getConfigManager().getConfig().isAscending), RESET, () -> ItemListOrderingConfig.REGISTRY_ASCENDING, config -> {
@@ -66,7 +67,7 @@ public class ClothScreenRegistry {
         });
         appearance.addOption(new IntegerSliderEntry("text.rei.config.max_recipes_per_page", 2, 99, RoughlyEnoughItemsCore.getConfigManager().getConfig().maxRecipePerPage, RESET, () -> 3, i -> RoughlyEnoughItemsCore.getConfigManager().getConfig().maxRecipePerPage = i, () -> getConfigTooltip("max_recipes_per_page")));
         appearance.addOption(new BooleanListEntry("text.rei.config.light_gray_recipe_border", RoughlyEnoughItemsCore.getConfigManager().getConfig().lightGrayRecipeBorder, RESET, () -> false, bool -> RoughlyEnoughItemsCore.getConfigManager().getConfig().lightGrayRecipeBorder = bool, () -> getConfigTooltip("light_gray_recipe_border")));
-        appearance.addOption(new BooleanListEntry("text.rei.config.prefer_visible_recipes", RoughlyEnoughItemsCore.getConfigManager().getConfig().preferVisibleRecipes, RESET, () -> false, bool -> RoughlyEnoughItemsCore.getConfigManager().getConfig().preferVisibleRecipes = bool, () -> getConfigTooltip("prefer_visible_recipes")));
+        appearance.addOption(new BooleanListEntry("text.rei.config.villager_screen_permanent_scroll_bar", RoughlyEnoughItemsCore.getConfigManager().getConfig().villagerScreenPermanentScrollBar, RESET, () -> false, bool -> RoughlyEnoughItemsCore.getConfigManager().getConfig().villagerScreenPermanentScrollBar = bool, () -> getConfigTooltip("villager_screen_permanent_scroll_bar")));
         ConfigScreenBuilder.CategoryBuilder action = builder.addCategory("text.rei.config.action");
         action.addOption(new EnumListEntry<>("text.rei.config.item_cheating_mode", ItemCheatingMode.class, RoughlyEnoughItemsCore.getConfigManager().getConfig().itemCheatingMode, RESET, () -> ItemCheatingMode.REI_LIKE, i -> RoughlyEnoughItemsCore.getConfigManager().getConfig().itemCheatingMode = i, e -> {
             return I18n.translate("text.rei.config.item_cheating_mode." + e.name().toLowerCase());
@@ -74,12 +75,11 @@ public class ClothScreenRegistry {
         action.addOption(new StringListEntry("text.rei.give_command", RoughlyEnoughItemsCore.getConfigManager().getConfig().giveCommand, RESET, () -> "/give {player_name} {item_identifier}{nbt} {count}", s -> RoughlyEnoughItemsCore.getConfigManager().getConfig().giveCommand = s, () -> getConfigTooltip("give_command")));
         action.addOption(new StringListEntry("text.rei.gamemode_command", RoughlyEnoughItemsCore.getConfigManager().getConfig().gamemodeCommand, RESET, () -> "/gamemode {gamemode}", s -> RoughlyEnoughItemsCore.getConfigManager().getConfig().gamemodeCommand = s, () -> getConfigTooltip("gamemode_command")));
         action.addOption(new StringListEntry("text.rei.weather_command", RoughlyEnoughItemsCore.getConfigManager().getConfig().weatherCommand, RESET, () -> "/weather {weather}", s -> RoughlyEnoughItemsCore.getConfigManager().getConfig().weatherCommand = s, () -> getConfigTooltip("weather_command")));
+        action.addOption(new BooleanListEntry("text.rei.config.register_in_other_thread", RoughlyEnoughItemsCore.getConfigManager().getConfig().registerRecipesInAnotherThread, RESET, () -> true, bool -> RoughlyEnoughItemsCore.getConfigManager().getConfig().registerRecipesInAnotherThread = bool, () -> getConfigTooltip("register_in_other_thread")));
         ConfigScreenBuilder.CategoryBuilder modules = builder.addCategory("text.rei.config.modules");
         modules.addOption(new BooleanListEntry("text.rei.config.enable_craftable_only", RoughlyEnoughItemsCore.getConfigManager().getConfig().enableCraftableOnlyButton, RESET, () -> true, bool -> RoughlyEnoughItemsCore.getConfigManager().getConfig().enableCraftableOnlyButton = bool, () -> getConfigTooltip("enable_craftable_only")));
         modules.addOption(new BooleanListEntry("text.rei.config.enable_util_buttons", RoughlyEnoughItemsCore.getConfigManager().getConfig().showUtilsButtons, RESET, () -> false, bool -> RoughlyEnoughItemsCore.getConfigManager().getConfig().showUtilsButtons = bool, () -> getConfigTooltip("enable_util_buttons")));
         modules.addOption(new BooleanListEntry("text.rei.config.disable_recipe_book", RoughlyEnoughItemsCore.getConfigManager().getConfig().disableRecipeBook, RESET, () -> false, bool -> RoughlyEnoughItemsCore.getConfigManager().getConfig().disableRecipeBook = bool, () -> getConfigTooltip("disable_recipe_book")));
-        ConfigScreenBuilder.CategoryBuilder aprilFools = builder.addCategory("text.rei.config.april_fools");
-        aprilFools.addOption(new BooleanListEntry("text.rei.config.april_fools.2019", RoughlyEnoughItemsCore.getConfigManager().getConfig().aprilFoolsFish2019, RESET, () -> false, bool -> RoughlyEnoughItemsCore.getConfigManager().getConfig().aprilFoolsFish2019 = bool, () -> getConfigTooltip("april_fools.2019")));
         return builder.build(screen -> {
             ButtonWidget w = new ButtonWidget(6, 6, 60, 20, I18n.translate("text.rei.credits"), widget -> MinecraftClient.getInstance().openScreen(new CreditsScreen(MinecraftClient.getInstance().currentScreen)));
             ((ScreenHooks) screen).cloth_getButtonWidgets().add(0, w);

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

@@ -87,6 +87,8 @@
   "text.rei.config.enable_legacy_speedcraft_support": "Enable Legacy Plugin Support: ",
   "text.rei.config.april_fools": "April Fools",
   "text.rei.config.april_fools.2019": "Force 2019 REI April Fools' joke: ",
+  "text.rei.config.dark_theme": "Dark Mode:",
+  "text.rei.config.villager_screen_permanent_scroll_bar": "Villager Screen Permanent Scroll Bar:",
   "text.rei.config.item_cheating_mode": "Item Cheating Amount:",
   "text.rei.config.item_cheating_mode.rei_like": "Normal",
   "text.rei.config.item_cheating_mode.jei_like": "Reversed",
@@ -98,9 +100,11 @@
   "text.rei.config.recipe_screen_type.original": "Original",
   "text.rei.config.recipe_screen_type.villager": "Villager",
   "text.rei.select": "Select",
+  "text.rei.working_station": "Working Station",
+  "text.rei.recipe_id": "\n%sRecipe Id: %s",
+  "text.rei.config.register_in_other_thread": "Register Recipes in other thread:",
   "text.rei.recipe_screen_type.selection": "Recipe Screen Type Selection",
   "text.rei.recipe_screen_type.selection.sub": "You can always edit this setting again via the config screen.",
-
   "_comment": "Config Tooltips",
   "tooltip.rei.config.side_search_box": "Declares the location of the search field:\nYes: Left / Right\nNo: Center\n \nDefaulted: No",
   "tooltip.rei.config.list_ordering": "Declares the ordering of the side item list:\nValues: Registry / Name / Item Groups\n(Ascending / Descending)\n \nDefaulted: %s",
@@ -108,7 +112,7 @@
   "tooltip.rei.config.max_recipes_per_page": "Declares the maximum possible displayed recipes:\nValues: 2 - 99\n \nDefaulted: 3",
   "tooltip.rei.config.light_gray_recipe_border": "Declares the appearance of the recipe border:\nYes: Light Gray\nNo: Hard Black\n \nDefaulted: No",
   "tooltip.rei.config.april_fools.2019": "Forcefully enables the 2019 april fools joke:\nValues: Yes / No\n \nDefaulted: No",
-
+  "tooltip.rei.config.register_in_other_thread": "Requires datapacks to be reloaded.\nCan be done by rejoin the world / server",
   "_comment": "Don't change / translate the credit down below if you are doing it :)",
   "text.rei.credit.text": "§lRoughly Enough Items\n§7Originally a fork for Almost Enough Items.\n\n§lDevelopers\n  - Originally by ZenDarva\n  - Created by Danielshe\n  - Plugin Support by TehNut\n\n§lLanguage Translation\n  English - Danielshe\n  Simplified Chinese - XuyuEre & Danielshe\n  Traditional Chinese - hugoalh, gxy17886 & Danielshe\n  French - Yanis48\n  German - MelanX\n  Estonian - Madis0\n  Portuguese - thiagokenis\n  LOLCAT - Danielshe\n  Upside Down - Danielshe\n  Brazilian Portuguese - thiagokenis\n  Bulgarian - geniiii\n\n§lLicense\n§7Roughly Enough Items is using MIT."
 }

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

@@ -3,6 +3,8 @@
   "key.roughlyenoughitems.recipe_keybind": "显示配方",
   "key.roughlyenoughitems.hide_keybind": "隐藏/显示 REI",
   "key.roughlyenoughitems.usage_keybind": "显示用途",
+  "key.roughlyenoughitems.next_page": "下一页",
+  "key.roughlyenoughitems.previous_page": "上一页",
   "text.rei.config.general": "常规",
   "text.rei.config.action": "功能",
   "text.rei.config.cheating": "作弊:",
@@ -38,7 +40,7 @@
   "ordering.rei.descending": "倒序",
   "ordering.rei.registry": "注册",
   "ordering.rei.name": "名字",
-  "ordering.rei.item_groups": "物品分类",
+  "ordering.rei.item_groups": "物品",
   "text.speed_craft.failed_move_items": "§c不能移动物品!",
   "text.speed_craft.move_items": "移动物品",
   "text.rei.config.enable_craftable_only": "过滤不可合成的物品: ",
@@ -85,10 +87,26 @@
   "text.rei.config.enable_legacy_speedcraft_support": "启用旧版插件支持: ",
   "text.rei.config.april_fools": "愚人节",
   "text.rei.config.april_fools.2019": "强制启用REI 2019愚人节玩笑: ",
+  "text.rei.config.dark_theme": "深色模式:",
+  "text.rei.config.villager_screen_permanent_scroll_bar": "村民界面一直显示滚动条:",
   "text.rei.config.item_cheating_mode": "物品作弊数量模式:",
   "text.rei.config.item_cheating_mode.rei_like": "标准",
   "text.rei.config.item_cheating_mode.jei_like": "反转",
   "text.rei.config.light_gray_recipe_border": "浅灰色配方边框:",
   "text.rei.config_api_failed": "如果Cloth Config API装载失败了或你没有安装它,你就会到达这个界面!\n升级/安装API或向bug跟踪器报告.",
-  "text.rei.back": "返回"
+  "text.rei.back": "返回",
+  "text.rei.config.recipe_screen_type": "界面类型",
+  "text.rei.config.recipe_screen_type.unset": "未设置",
+  "text.rei.config.recipe_screen_type.original": "原生",
+  "text.rei.config.recipe_screen_type.villager": "类村民",
+  "text.rei.select": "选择",
+  "text.rei.recipe_screen_type.selection": "合成界面类型选择",
+  "text.rei.recipe_screen_type.selection.sub": "你始终可以通过配置界面再次编辑此设置.",
+  "_comment": "Config Tooltips",
+  "tooltip.rei.config.side_search_box": "声明搜索框的位置:\nYes: 左 / 右\nNo: 中央\n \n默认: No",
+  "tooltip.rei.config.list_ordering": "声明侧边物品列表的排序:\n值: 注册 / 名字 / 物品组\n(顺序 / 倒序)\n \n默认: %s",
+  "tooltip.rei.config.item_list_position": "声明侧边物品列表的位置:\n值: 左 / 右\n \n默认: 右",
+  "tooltip.rei.config.max_recipes_per_page": "声明每页显示的最大的配方数目:\n值: 2 - 99\n \n默认: 3",
+  "tooltip.rei.config.light_gray_recipe_border": "声明配方边框的外观D:\nYes: 浅灰色\nNo: 深黑色\n \n默认: No",
+  "tooltip.rei.config.april_fools.2019": "强制启用2019愚人节玩笑:\n值: Yes / No\n \n默认: No",
 }

BIN
src/main/resources/assets/roughlyenoughitems/textures/gui/button_dark.png


BIN
src/main/resources/assets/roughlyenoughitems/textures/gui/display_dark.png


BIN
src/main/resources/assets/roughlyenoughitems/textures/gui/recipecontainer.png


BIN
src/main/resources/assets/roughlyenoughitems/textures/gui/recipecontainer_dark.png