瀏覽代碼

Merge pull request #86 from shedaniel/1.14-dev

REi v2.9 (WIP)
Daniel She 6 年之前
父節點
當前提交
7bcf60309a
共有 40 個文件被更改,包括 1158 次插入244 次删除
  1. 10 0
      CHANGELOG.md
  2. 1 1
      gradle.properties
  3. 38 0
      src/main/java/com/zeitheron/hammercore/client/utils/Scissors.java
  4. 8 0
      src/main/java/me/shedaniel/rei/api/ClientHelper.java
  5. 9 0
      src/main/java/me/shedaniel/rei/api/RecipeCategory.java
  6. 52 0
      src/main/java/me/shedaniel/rei/api/Renderable.java
  7. 13 0
      src/main/java/me/shedaniel/rei/api/Renderer.java
  8. 25 3
      src/main/java/me/shedaniel/rei/client/ClientHelperImpl.java
  9. 2 0
      src/main/java/me/shedaniel/rei/client/ConfigObject.java
  10. 16 0
      src/main/java/me/shedaniel/rei/client/RecipeScreenType.java
  11. 8 14
      src/main/java/me/shedaniel/rei/gui/ContainerScreenOverlay.java
  12. 133 0
      src/main/java/me/shedaniel/rei/gui/PreRecipeViewingScreen.java
  13. 7 8
      src/main/java/me/shedaniel/rei/gui/RecipeViewingScreen.java
  14. 270 0
      src/main/java/me/shedaniel/rei/gui/VillagerRecipeViewingScreen.java
  15. 13 0
      src/main/java/me/shedaniel/rei/gui/renderables/EmptyRenderer.java
  16. 69 0
      src/main/java/me/shedaniel/rei/gui/renderables/ItemStackRenderer.java
  17. 13 0
      src/main/java/me/shedaniel/rei/gui/renderables/RecipeRenderer.java
  18. 87 0
      src/main/java/me/shedaniel/rei/gui/renderables/SimpleRecipeRenderer.java
  19. 2 2
      src/main/java/me/shedaniel/rei/gui/widget/ButtonWidget.java
  20. 5 0
      src/main/java/me/shedaniel/rei/gui/widget/CategoryBaseWidget.java
  21. 10 2
      src/main/java/me/shedaniel/rei/gui/widget/ClickableLabelWidget.java
  22. 6 6
      src/main/java/me/shedaniel/rei/gui/widget/ItemListOverlay.java
  23. 10 143
      src/main/java/me/shedaniel/rei/gui/widget/ItemSlotWidget.java
  24. 12 2
      src/main/java/me/shedaniel/rei/gui/widget/RecipeBaseWidget.java
  25. 26 0
      src/main/java/me/shedaniel/rei/gui/widget/SlotBaseWidget.java
  26. 203 0
      src/main/java/me/shedaniel/rei/gui/widget/SlotWidget.java
  27. 19 24
      src/main/java/me/shedaniel/rei/gui/widget/TabWidget.java
  28. 11 4
      src/main/java/me/shedaniel/rei/plugin/DefaultBlastingCategory.java
  29. 7 7
      src/main/java/me/shedaniel/rei/plugin/DefaultBrewingCategory.java
  30. 3 3
      src/main/java/me/shedaniel/rei/plugin/DefaultCampfireCategory.java
  31. 4 4
      src/main/java/me/shedaniel/rei/plugin/DefaultCraftingCategory.java
  32. 31 8
      src/main/java/me/shedaniel/rei/plugin/DefaultPlugin.java
  33. 11 4
      src/main/java/me/shedaniel/rei/plugin/DefaultSmeltingCategory.java
  34. 11 4
      src/main/java/me/shedaniel/rei/plugin/DefaultSmokingCategory.java
  35. 3 3
      src/main/java/me/shedaniel/rei/plugin/DefaultStoneCuttingCategory.java
  36. 2 0
      src/main/java/me/shedaniel/rei/utils/ClothScreenRegistry.java
  37. 7 1
      src/main/resources/assets/roughlyenoughitems/lang/en_us.json
  38. 二進制
      src/main/resources/assets/roughlyenoughitems/textures/gui/recipecontainer.png
  39. 二進制
      src/main/resources/assets/roughlyenoughitems/textures/gui/screenshot.png
  40. 1 1
      src/main/resources/fabric.mod.json

+ 10 - 0
CHANGELOG.md

@@ -1,4 +1,14 @@
 View full changelog [here](https://github.com/shedaniel/RoughlyEnoughItems/blob/1.14/CHANGELOG.md).
+## v2.9-beta+build.106 (BETA)
+- Using: [HammerLib](https://minecraft.curseforge.com/projects/hammer-lib) as a simple opengl scissors api
+- New: Mod Name of category new shows in category tooltips
+- New: Renderer API
+- New: Villager Trading Like Recipe Screen. ![image](https://cdn.discordapp.com/attachments/432055962233470988/576077676305973275/unknown.png)
+- New: Recipe Screen Selection Screen
+- Removed: All `Locale.ROOT` usage
+- Fixed: Button Width Rendering issues when the width is an odd number
+- Changed: Mod Name changed from `RoughlyEnoughItems` to `Roughly Enough Items`
+- Changed: Lots of internal refractors
 ## v2.8.2+build.104
 - Fixed [#81](https://github.com/shedaniel/RoughlyEnoughItems/issues/81): Scrolling unaffected by exclusion zones.
 - Added [#82](https://github.com/shedaniel/RoughlyEnoughItems/issues/82): Close search after pressing "Enter"

+ 1 - 1
gradle.properties

@@ -1,4 +1,4 @@
-mod_version=2.8.2+build.105
+mod_version=2.9-beta+build.106
 minecraft_version=1.14
 yarn_version=1.14+build.5
 fabricloader_version=0.4.6+build.141

+ 38 - 0
src/main/java/com/zeitheron/hammercore/client/utils/Scissors.java

@@ -0,0 +1,38 @@
+package com.zeitheron.hammercore.client.utils;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.util.Window;
+import org.lwjgl.opengl.GL11;
+
+/**
+ * This is originally the part of Hammer Lib, repacked in REI with permission.
+ * Adapted GL scissor for minecraft pixel resolution and adjusts (0;0) as left-top corner.
+ *
+ * @author Zeitheron
+ */
+public class Scissors {
+    public static void begin() {
+        GL11.glEnable(GL11.GL_SCISSOR_TEST);
+    }
+    
+    public static void scissor(int x, int y, int width, int height) {
+        Window window = MinecraftClient.getInstance().window;
+        
+        int sw = window.getWidth();
+        int sh = window.getHeight();
+        float dw = window.getScaledWidth();
+        float dh = window.getScaledHeight();
+        
+        x = Math.round(sw * (x / dw));
+        y = Math.round(sh * (y / dh));
+        
+        width = Math.round(sw * (width / dw));
+        height = Math.round(sh * (height / dh));
+        
+        GL11.glScissor(x, sh - height - y, width, height);
+    }
+    
+    public static void end() {
+        GL11.glDisable(GL11.GL_SCISSOR_TEST);
+    }
+}

+ 8 - 0
src/main/java/me/shedaniel/rei/api/ClientHelper.java

@@ -5,8 +5,10 @@ import net.fabricmc.api.ClientModInitializer;
 import net.fabricmc.fabric.api.client.keybinding.FabricKeyBinding;
 import net.minecraft.item.Item;
 import net.minecraft.item.ItemStack;
+import net.minecraft.util.Identifier;
 
 import java.util.List;
+import java.util.Map;
 
 public interface ClientHelper extends ClientModInitializer {
     static ClientHelper getInstance() {
@@ -19,6 +21,8 @@ public interface ClientHelper extends ClientModInitializer {
     
     List<ItemStack> getInventoryItemsTypes();
     
+    void openRecipeViewingScreen(Map<RecipeCategory, List<RecipeDisplay>> map);
+    
     void registerFabricKeyBinds();
     
     boolean tryCheatingStack(ItemStack stack);
@@ -33,6 +37,10 @@ public interface ClientHelper extends ClientModInitializer {
     
     String getFormattedModFromItem(Item item);
     
+    String getFormattedModFromIdentifier(Identifier identifier);
+    
+    String getModFromIdentifier(Identifier identifier);
+    
     FabricKeyBinding getRecipeKeyBinding();
     
     FabricKeyBinding getUsageKeyBinding();

+ 9 - 0
src/main/java/me/shedaniel/rei/api/RecipeCategory.java

@@ -1,6 +1,7 @@
 package me.shedaniel.rei.api;
 
 import me.shedaniel.rei.gui.RecipeViewingScreen;
+import me.shedaniel.rei.gui.renderables.RecipeRenderer;
 import me.shedaniel.rei.gui.widget.CategoryBaseWidget;
 import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
 import me.shedaniel.rei.gui.widget.Widget;
@@ -20,8 +21,16 @@ public interface RecipeCategory<T extends RecipeDisplay> {
     
     ItemStack getCategoryIcon();
     
+    default Renderer getIcon() {
+        return Renderable.fromItemStackSupplier(this::getCategoryIcon);
+    }
+    
     String getCategoryName();
     
+    default RecipeRenderer getSimpleRenderer(T recipe) {
+        return Renderable.fromRecipe(recipe::getInput, recipe::getOutput);
+    }
+    
     default List<Widget> setupDisplay(Supplier<T> recipeDisplaySupplier, Rectangle bounds) {
         return Collections.singletonList(new RecipeBaseWidget(bounds));
     }

+ 52 - 0
src/main/java/me/shedaniel/rei/api/Renderable.java

@@ -0,0 +1,52 @@
+package me.shedaniel.rei.api;
+
+import me.shedaniel.rei.gui.renderables.EmptyRenderer;
+import me.shedaniel.rei.gui.renderables.ItemStackRenderer;
+import me.shedaniel.rei.gui.renderables.SimpleRecipeRenderer;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.math.MathHelper;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+public interface Renderable {
+    
+    static ItemStackRenderer fromItemStackSupplier(Supplier<ItemStack> supplier) {
+        return new ItemStackRenderer() {
+            @Override
+            public ItemStack getItemStack() {
+                return supplier.get();
+            }
+        };
+    }
+    
+    static ItemStackRenderer fromItemStack(ItemStack stack) {
+        return new ItemStackRenderer() {
+            @Override
+            public ItemStack getItemStack() {
+                return stack;
+            }
+        };
+    }
+    
+    static EmptyRenderer empty() {
+        return EmptyRenderer.INSTANCE;
+    }
+    
+    static SimpleRecipeRenderer fromRecipe(Supplier<List<List<ItemStack>>> input, Supplier<List<ItemStack>> output) {
+        return new SimpleRecipeRenderer(input, output);
+    }
+    
+    static ItemStackRenderer fromItemStacks(List<ItemStack> stacks) {
+        return new ItemStackRenderer() {
+            @Override
+            public ItemStack getItemStack() {
+                if (stacks.isEmpty())
+                    return ItemStack.EMPTY;
+                return stacks.get(MathHelper.floor((System.currentTimeMillis() / 500 % (double) stacks.size()) / 1f));
+            }
+        };
+    }
+    
+    void render(int x, int y, double mouseX, double mouseY, float delta);
+}

+ 13 - 0
src/main/java/me/shedaniel/rei/api/Renderer.java

@@ -0,0 +1,13 @@
+package me.shedaniel.rei.api;
+
+import net.minecraft.client.gui.DrawableHelper;
+
+public abstract class Renderer extends DrawableHelper implements Renderable {
+    public int getBlitOffset() {
+        return this.blitOffset;
+    }
+    
+    public void setBlitOffset(int offset) {
+        this.blitOffset = offset;
+    }
+}

+ 25 - 3
src/main/java/me/shedaniel/rei/client/ClientHelperImpl.java

@@ -9,7 +9,9 @@ import me.shedaniel.rei.api.ClientHelper;
 import me.shedaniel.rei.api.RecipeCategory;
 import me.shedaniel.rei.api.RecipeDisplay;
 import me.shedaniel.rei.api.RecipeHelper;
+import me.shedaniel.rei.gui.PreRecipeViewingScreen;
 import me.shedaniel.rei.gui.RecipeViewingScreen;
+import me.shedaniel.rei.gui.VillagerRecipeViewingScreen;
 import net.fabricmc.fabric.api.client.keybinding.FabricKeyBinding;
 import net.fabricmc.fabric.api.network.ClientSidePacketRegistry;
 import net.fabricmc.fabric.impl.client.keybinding.KeyBindingRegistryImpl;
@@ -53,6 +55,14 @@ public class ClientHelperImpl implements ClientHelper {
         return "§9§o" + mod;
     }
     
+    @Override
+    public String getFormattedModFromIdentifier(Identifier identifier) {
+        String mod = getModFromIdentifier(identifier);
+        if (mod.equalsIgnoreCase(""))
+            return "";
+        return "§9§o" + mod;
+    }
+    
     @Override
     public FabricKeyBinding getRecipeKeyBinding() {
         return recipe;
@@ -78,12 +88,14 @@ public class ClientHelperImpl implements ClientHelper {
         return nextPage;
     }
     
+    @Override
     public String getModFromItem(Item item) {
         if (item.equals(Items.AIR))
             return "";
         return getModFromIdentifier(Registry.ITEM.getId(item));
     }
     
+    @Override
     public String getModFromIdentifier(Identifier identifier) {
         if (identifier == null)
             return "";
@@ -147,7 +159,7 @@ public class ClientHelperImpl implements ClientHelper {
     public boolean executeRecipeKeyBind(ItemStack stack) {
         Map<RecipeCategory, List<RecipeDisplay>> map = RecipeHelper.getInstance().getRecipesFor(stack);
         if (map.keySet().size() > 0)
-            MinecraftClient.getInstance().openScreen(new RecipeViewingScreen(MinecraftClient.getInstance().window, map));
+            openRecipeViewingScreen(map);
         return map.keySet().size() > 0;
     }
     
@@ -155,7 +167,7 @@ public class ClientHelperImpl implements ClientHelper {
     public boolean executeUsageKeyBind(ItemStack stack) {
         Map<RecipeCategory, List<RecipeDisplay>> map = RecipeHelper.getInstance().getUsagesFor(stack);
         if (map.keySet().size() > 0)
-            MinecraftClient.getInstance().openScreen(new RecipeViewingScreen(MinecraftClient.getInstance().window, map));
+            openRecipeViewingScreen(map);
         return map.keySet().size() > 0;
     }
     
@@ -174,10 +186,20 @@ public class ClientHelperImpl implements ClientHelper {
     public boolean executeViewAllRecipesKeyBind() {
         Map<RecipeCategory, List<RecipeDisplay>> map = RecipeHelper.getInstance().getAllRecipes();
         if (map.keySet().size() > 0)
-            MinecraftClient.getInstance().openScreen(new RecipeViewingScreen(MinecraftClient.getInstance().window, map));
+            openRecipeViewingScreen(map);
         return map.keySet().size() > 0;
     }
     
+    @Override
+    public void openRecipeViewingScreen(Map<RecipeCategory, List<RecipeDisplay>> map) {
+        if (RoughlyEnoughItemsCore.getConfigManager().getConfig().screenType == RecipeScreenType.VILLAGER)
+            MinecraftClient.getInstance().openScreen(new VillagerRecipeViewingScreen(map));
+        else if (RoughlyEnoughItemsCore.getConfigManager().getConfig().screenType == RecipeScreenType.UNSET)
+            MinecraftClient.getInstance().openScreen(new PreRecipeViewingScreen(map));
+        else
+            MinecraftClient.getInstance().openScreen(new RecipeViewingScreen(map));
+    }
+    
     @Override
     public void onInitializeClient() {
         ClientHelperImpl.instance = (ClientHelperImpl) this;

+ 2 - 0
src/main/java/me/shedaniel/rei/client/ConfigObject.java

@@ -48,6 +48,8 @@ public class ConfigObject {
     
     public boolean lightGrayRecipeBorder = false;
     
+    public RecipeScreenType screenType = RecipeScreenType.UNSET;
+    
     @Comment(
             "The location of choose page dialog, will automatically be set to your last location so there is no need to change this.")
     public RelativePoint choosePageDialogPoint = new RelativePoint(.5, .5);

+ 16 - 0
src/main/java/me/shedaniel/rei/client/RecipeScreenType.java

@@ -0,0 +1,16 @@
+package me.shedaniel.rei.client;
+
+import net.minecraft.client.resource.language.I18n;
+
+import java.util.Locale;
+
+public enum RecipeScreenType {
+    UNSET,
+    ORIGINAL,
+    VILLAGER;
+    
+    @Override
+    public String toString() {
+        return I18n.translate("text.rei.config.recipe_screen_type." + name().toLowerCase());
+    }
+}

+ 8 - 14
src/main/java/me/shedaniel/rei/gui/ContainerScreenOverlay.java

@@ -27,13 +27,15 @@ import net.minecraft.util.math.MathHelper;
 import net.minecraft.world.GameMode;
 
 import java.awt.*;
-import java.util.*;
+import java.util.LinkedList;
 import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
 import java.util.stream.Collectors;
 
 public class ContainerScreenOverlay extends AbstractParentElement implements Drawable {
     
-    private static final Identifier CHEST_GUI_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui" + "/recipecontainer.png");
+    private static final Identifier CHEST_GUI_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
     private static final List<QueuedTooltip> QUEUED_TOOLTIPS = Lists.newArrayList();
     public static String searchTerm = "";
     private static int page = 0;
@@ -43,7 +45,6 @@ public class ContainerScreenOverlay extends AbstractParentElement implements Dra
     private Window window;
     private CraftableToggleButtonWidget toggleButtonWidget;
     private ButtonWidget buttonLeft, buttonRight;
-    private int lastLeft;
     
     public static ItemListOverlay getItemListOverlay() {
         return itemListOverlay;
@@ -59,7 +60,6 @@ public class ContainerScreenOverlay extends AbstractParentElement implements Dra
         this.window = MinecraftClient.getInstance().window;
         DisplayHelper.DisplayBoundsHandler boundsHandler = RoughlyEnoughItemsCore.getDisplayHelper().getResponsibleBoundsHandler(MinecraftClient.getInstance().currentScreen.getClass());
         this.rectangle = RoughlyEnoughItemsCore.getConfigManager().getConfig().mirrorItemPanel ? boundsHandler.getLeftBounds(MinecraftClient.getInstance().currentScreen) : boundsHandler.getRightBounds(MinecraftClient.getInstance().currentScreen);
-        this.lastLeft = getLeft();
         widgets.add(itemListOverlay = new ItemListOverlay(page));
         itemListOverlay.updateList(boundsHandler, boundsHandler.getItemListArea(rectangle), page, searchTerm, false);
         
@@ -306,6 +306,10 @@ public class ContainerScreenOverlay extends AbstractParentElement implements Dra
             RecipeViewingScreen widget = (RecipeViewingScreen) MinecraftClient.getInstance().currentScreen;
             return new Rectangle(widget.getBounds().x, window.getScaledHeight() - 22, widget.getBounds().width - widthRemoved, 18);
         }
+        if (MinecraftClient.getInstance().currentScreen instanceof VillagerRecipeViewingScreen) {
+            VillagerRecipeViewingScreen widget = (VillagerRecipeViewingScreen) MinecraftClient.getInstance().currentScreen;
+            return new Rectangle(widget.bounds.x, window.getScaledHeight() - 22, widget.bounds.width - widthRemoved, 18);
+        }
         return new Rectangle(ScreenHelper.getLastContainerScreenHooks().rei_getContainerLeft(), window.getScaledHeight() - 22, ScreenHelper.getLastContainerScreenHooks().rei_getContainerWidth() - widthRemoved, 18);
     }
     
@@ -427,16 +431,6 @@ public class ContainerScreenOverlay extends AbstractParentElement implements Dra
         GuiLighting.disable();
     }
     
-    private int getLeft() {
-        if (MinecraftClient.getInstance().currentScreen instanceof RecipeViewingScreen) {
-            RecipeViewingScreen widget = (RecipeViewingScreen) MinecraftClient.getInstance().currentScreen;
-            return widget.getBounds().x;
-        }
-        if (MinecraftClient.getInstance().player.getRecipeBook().isGuiOpen())
-            return ScreenHelper.getLastContainerScreenHooks().rei_getContainerLeft() - 147 - 30;
-        return ScreenHelper.getLastContainerScreenHooks().rei_getContainerLeft();
-    }
-    
     private int getTotalPage() {
         return itemListOverlay.getTotalPage();
     }

+ 133 - 0
src/main/java/me/shedaniel/rei/gui/PreRecipeViewingScreen.java

@@ -0,0 +1,133 @@
+package me.shedaniel.rei.gui;
+
+import com.google.common.collect.Lists;
+import me.shedaniel.rei.RoughlyEnoughItemsCore;
+import me.shedaniel.rei.api.ClientHelper;
+import me.shedaniel.rei.api.RecipeCategory;
+import me.shedaniel.rei.api.RecipeDisplay;
+import me.shedaniel.rei.client.RecipeScreenType;
+import me.shedaniel.rei.client.ScreenHelper;
+import me.shedaniel.rei.gui.widget.ButtonWidget;
+import me.shedaniel.rei.gui.widget.HighlightableWidget;
+import me.shedaniel.rei.gui.widget.Widget;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.audio.PositionedSoundInstance;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.Screen;
+import net.minecraft.client.render.GuiLighting;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.sound.SoundEvents;
+import net.minecraft.text.TranslatableTextComponent;
+import net.minecraft.util.Identifier;
+
+import java.awt.*;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class PreRecipeViewingScreen extends Screen {
+    
+    private static final Identifier IDENTIFIER = new Identifier("roughlyenoughitems", "textures/gui/screenshot.png");
+    private final List<Widget> widgets;
+    private boolean original;
+    private Map<RecipeCategory, List<RecipeDisplay>> map;
+    
+    public PreRecipeViewingScreen(Map<RecipeCategory, List<RecipeDisplay>> map) {
+        super(new TranslatableTextComponent("text.rei.recipe_screen_type.selection"));
+        this.widgets = Lists.newArrayList();
+        this.original = true;
+        this.map = map;
+    }
+    
+    @Override
+    protected void init() {
+        this.children.clear();
+        this.widgets.clear();
+        this.widgets.add(new ButtonWidget(width / 2 - 100, height - 40, 200, 20, I18n.translate("text.rei.select")) {
+            @Override
+            public void onPressed() {
+                RoughlyEnoughItemsCore.getConfigManager().getConfig().screenType = original ? RecipeScreenType.ORIGINAL : RecipeScreenType.VILLAGER;
+                try {
+                    RoughlyEnoughItemsCore.getConfigManager().saveConfig();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+                ClientHelper.getInstance().openRecipeViewingScreen(map);
+            }
+        });
+        this.widgets.add(new ScreenTypeSelection(width / 2 - 200 - 5, height / 2 - 112 / 2 - 10, 0));
+        this.widgets.add(new ScreenTypeSelection(width / 2 + 5, height / 2 - 112 / 2 - 10, 112));
+        this.children.addAll(widgets);
+    }
+    
+    @Override
+    public void render(int int_1, int int_2, float float_1) {
+        this.renderBackground();
+        this.drawCenteredString(this.font, this.title.getFormattedText(), this.width / 2, 20, 16777215);
+        int i = 30;
+        for(String s : this.font.wrapStringToWidthAsList(I18n.translate("text.rei.recipe_screen_type.selection.sub"), width - 30)) {
+            this.drawCenteredString(this.font, "§7" + s, width / 2, i, -1);
+            i += 10;
+        }
+        super.render(int_1, int_2, float_1);
+        this.widgets.forEach(widget -> {
+            GuiLighting.disable();
+            widget.render(int_1, int_2, float_1);
+        });
+    }
+    
+    @Override
+    public boolean keyPressed(int int_1, int int_2, int int_3) {
+        if ((int_1 == 256 || this.minecraft.options.keyInventory.matchesKey(int_1, int_2)) && this.shouldCloseOnEsc()) {
+            MinecraftClient.getInstance().openScreen(ScreenHelper.getLastContainerScreen());
+            ScreenHelper.getLastOverlay().init();
+            return true;
+        }
+        return super.keyPressed(int_1, int_2, int_3);
+    }
+    
+    public class ScreenTypeSelection extends HighlightableWidget {
+        
+        private Rectangle bounds;
+        private int u, v;
+        
+        public ScreenTypeSelection(int x, int y, int v) {
+            this.bounds = new Rectangle(x - 4, y - 4, 208, 120);
+            this.u = 0;
+            this.v = v;
+        }
+        
+        @Override
+        public Rectangle getBounds() {
+            return bounds;
+        }
+        
+        @Override
+        public void render(int i, int i1, float delta) {
+            MinecraftClient.getInstance().getTextureManager().bindTexture(IDENTIFIER);
+            blit(bounds.x + 4, bounds.y + 4, u, v, 200, 112);
+            if (original == (v == 0)) {
+                fillGradient(bounds.x, bounds.y, bounds.x + bounds.width, bounds.y + 2, 0xFFFFFFFF, 0xFFFFFFFF);
+                fillGradient(bounds.x, bounds.y + bounds.height - 2, bounds.x + bounds.width, bounds.y + bounds.height, 0xFFFFFFFF, 0xFFFFFFFF);
+                fillGradient(bounds.x, bounds.y, bounds.x + 2, bounds.y + bounds.height, 0xFFFFFFFF, 0xFFFFFFFF);
+                fillGradient(bounds.x + bounds.width - 2, bounds.y, bounds.x + bounds.width, bounds.y + bounds.height, 0xFFFFFFFF, 0xFFFFFFFF);
+            }
+        }
+        
+        @Override
+        public boolean mouseClicked(double double_1, double double_2, int int_1) {
+            if (isHighlighted(double_1, double_2)) {
+                minecraft.getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
+                original = (v == 0);
+                return true;
+            }
+            return false;
+        }
+        
+        @Override
+        public List<? extends Element> children() {
+            return Collections.emptyList();
+        }
+    }
+}

+ 7 - 8
src/main/java/me/shedaniel/rei/gui/RecipeViewingScreen.java

@@ -43,16 +43,15 @@ public class RecipeViewingScreen extends Screen {
     public int largestWidth, largestHeight;
     public boolean choosePageActivated;
     public RecipeChoosePageWidget recipeChoosePageWidget;
-    private Window window;
     private Rectangle bounds;
     private RecipeCategory selectedCategory;
     private ButtonWidget recipeBack, recipeNext, categoryBack, categoryNext;
     
-    public RecipeViewingScreen(Window window, Map<RecipeCategory, List<RecipeDisplay>> categoriesMap) {
+    public RecipeViewingScreen(Map<RecipeCategory, List<RecipeDisplay>> categoriesMap) {
         super(new StringTextComponent(""));
         this.categoryPages = 0;
-        this.window = window;
         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);
         this.categoriesMap = categoriesMap;
         this.categories = Lists.newArrayList();
@@ -119,11 +118,11 @@ public class RecipeViewingScreen extends Screen {
         this.children.clear();
         this.tabs.clear();
         this.widgets.clear();
-        this.largestWidth = window.getScaledWidth() - 100;
-        this.largestHeight = window.getScaledHeight() - 40;
+        this.largestWidth = width - 100;
+        this.largestHeight = height - 40;
         this.guiWidth = MathHelper.clamp(getCurrentDisplayed().stream().map(display -> selectedCategory.getDisplayWidth(display)).max(Integer::compareTo).orElse(150) + 30, 0, largestWidth);
         this.guiHeight = MathHelper.floor(MathHelper.clamp((selectedCategory.getDisplayHeight() + 7d) * (getRecipesPerPage() + 1d) + 40d, 186d, (double) largestHeight));
-        this.bounds = new Rectangle(window.getScaledWidth() / 2 - guiWidth / 2, window.getScaledHeight() / 2 - guiHeight / 2, guiWidth, guiHeight);
+        this.bounds = new Rectangle(width / 2 - guiWidth / 2, height / 2 - guiHeight / 2, guiWidth, guiHeight);
         this.page = MathHelper.clamp(page, 0, getTotalPages(selectedCategory) - 1);
         
         widgets.add(categoryBack = new ButtonWidget((int) bounds.getX() + 5, (int) bounds.getY() + 5, 12, 12, new TranslatableTextComponent("text.rei.left_arrow")) {
@@ -235,7 +234,7 @@ public class RecipeViewingScreen extends Screen {
             int j = i + categoryPages * 6;
             if (categories.size() > j) {
                 TabWidget tab;
-                tabs.add(tab = new TabWidget(i, this, new Rectangle(bounds.x + 4 + 28 * i, bounds.y - 28, 28, 28)) {
+                tabs.add(tab = new TabWidget(i, new Rectangle(bounds.x + 4 + 28 * i, bounds.y - 28, 28, 28)) {
                     @Override
                     public boolean mouseClicked(double mouseX, double mouseY, int button) {
                         if (getBounds().contains(mouseX, mouseY)) {
@@ -250,7 +249,7 @@ public class RecipeViewingScreen extends Screen {
                         return false;
                     }
                 });
-                tab.setItem(categories.get(j).getCategoryIcon(), categories.get(j).getCategoryName(), tab.getId() + categoryPages * 6 == categories.indexOf(selectedCategory));
+                tab.setRenderer(categories.get(j), categories.get(j).getIcon(), categories.get(j).getCategoryName(), tab.getId() + categoryPages * 6 == categories.indexOf(selectedCategory));
             }
         }
         Optional<ButtonAreaSupplier> supplier = RecipeHelper.getInstance().getSpeedCraftButtonArea(selectedCategory);

+ 270 - 0
src/main/java/me/shedaniel/rei/gui/VillagerRecipeViewingScreen.java

@@ -0,0 +1,270 @@
+package me.shedaniel.rei.gui;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.mojang.blaze3d.platform.GlStateManager;
+import com.zeitheron.hammercore.client.utils.Scissors;
+import me.shedaniel.rei.api.*;
+import me.shedaniel.rei.client.ScreenHelper;
+import me.shedaniel.rei.gui.renderables.RecipeRenderer;
+import me.shedaniel.rei.gui.widget.*;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.audio.PositionedSoundInstance;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.Screen;
+import net.minecraft.client.render.GuiLighting;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.sound.SoundEvents;
+import net.minecraft.text.StringTextComponent;
+import net.minecraft.util.math.MathHelper;
+import org.lwjgl.BufferUtils;
+import org.lwjgl.glfw.GLFW;
+import org.lwjgl.opengl.GL11;
+
+import java.awt.*;
+import java.nio.IntBuffer;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import static me.shedaniel.rei.gui.RecipeViewingScreen.getSpeedCraftFunctionalByCategory;
+
+public class VillagerRecipeViewingScreen extends Screen {
+    
+    private final Map<RecipeCategory, List<RecipeDisplay>> categoryMap;
+    private final List<RecipeCategory> categories;
+    private final List<Widget> widgets;
+    private final List<ButtonWidget> buttonWidgets;
+    private final List<Renderer> recipeRenderers;
+    private final List<TabWidget> tabs;
+    public Rectangle bounds, scrollListBounds;
+    private int selectedCategoryIndex, selectedRecipeIndex;
+    private double scroll;
+    private int tabsPage;
+    private static final int TABS_PER_PAGE = 9;
+    
+    public VillagerRecipeViewingScreen(Map<RecipeCategory, List<RecipeDisplay>> map) {
+        super(new StringTextComponent(""));
+        this.widgets = Lists.newArrayList();
+        this.categoryMap = Maps.newLinkedHashMap();
+        this.selectedCategoryIndex = 0;
+        this.selectedRecipeIndex = 0;
+        this.scroll = 0;
+        this.tabsPage = 0;
+        this.categories = Lists.newArrayList();
+        this.buttonWidgets = Lists.newArrayList();
+        this.tabs = Lists.newArrayList();
+        this.recipeRenderers = Lists.newArrayList();
+        RecipeHelper.getInstance().getAllCategories().forEach(category -> {
+            if (map.containsKey(category)) {
+                categories.add(category);
+                categoryMap.put(category, map.get(category));
+            }
+        });
+    }
+    
+    @Override
+    protected void init() {
+        super.init();
+        this.children.clear();
+        this.widgets.clear();
+        this.buttonWidgets.clear();
+        this.recipeRenderers.clear();
+        this.tabs.clear();
+        int largestWidth = width - 100;
+        int largestHeight = height - 40;
+        RecipeCategory category = categories.get(selectedCategoryIndex);
+        RecipeDisplay display = categoryMap.get(category).get(selectedRecipeIndex);
+        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);
+        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));
+        
+        Rectangle recipeBounds = new Rectangle(bounds.x + 100 + (guiWidth - 100) / 2 - category.getDisplayWidth(display) / 2, bounds.y + bounds.height / 2 - category.getDisplayHeight() / 2, category.getDisplayWidth(display), category.getDisplayHeight());
+        this.widgets.addAll(category.setupDisplay(() -> display, recipeBounds));
+        Optional<ButtonAreaSupplier> supplier = RecipeHelper.getInstance().getSpeedCraftButtonArea(category);
+        final SpeedCraftFunctional functional = getSpeedCraftFunctionalByCategory(ScreenHelper.getLastContainerScreen(), category);
+        if (supplier.isPresent())
+            this.widgets.add(new SpeedCraftingButtonWidget(supplier.get().get(recipeBounds), supplier.get().getButtonText(), functional, () -> display));
+        
+        int index = 0;
+        for(RecipeDisplay recipeDisplay : categoryMap.get(category)) {
+            int finalIndex = index;
+            RecipeRenderer recipeRenderer;
+            recipeRenderers.add(recipeRenderer = category.getSimpleRenderer(recipeDisplay));
+            buttonWidgets.add(new ButtonWidget(bounds.x + 5, 0, recipeRenderer.getWidth(), recipeRenderer.getHeight(), "") {
+                @Override
+                public void onPressed() {
+                    selectedRecipeIndex = finalIndex;
+                    VillagerRecipeViewingScreen.this.init();
+                }
+            });
+            index++;
+        }
+        for(int i = 0; i < TABS_PER_PAGE; i++) {
+            int j = i + tabsPage * TABS_PER_PAGE;
+            if (categories.size() > j) {
+                TabWidget tab;
+                tabs.add(tab = new TabWidget(i, new Rectangle(bounds.x + bounds.width / 2 - Math.min(categories.size() - tabsPage * TABS_PER_PAGE, TABS_PER_PAGE) * 14 + i * 28, bounds.y - 28, 28, 28)) {
+                    @Override
+                    public boolean mouseClicked(double mouseX, double mouseY, int button) {
+                        if (getBounds().contains(mouseX, mouseY)) {
+                            MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
+                            if (getId() + tabsPage * TABS_PER_PAGE == selectedCategoryIndex)
+                                return false;
+                            selectedCategoryIndex = getId() + tabsPage * TABS_PER_PAGE;
+                            scroll = 0;
+                            VillagerRecipeViewingScreen.this.init();
+                            return true;
+                        }
+                        return false;
+                    }
+                });
+                tab.setRenderer(categories.get(j), categories.get(j).getIcon(), categories.get(j).getCategoryName(), tab.getId() + tabsPage * TABS_PER_PAGE == selectedCategoryIndex);
+            }
+        }
+        
+        this.widgets.add(new ClickableLabelWidget(bounds.x + 4 + scrollListBounds.width / 2, bounds.y + 6, categories.get(selectedCategoryIndex).getCategoryName()) {
+            @Override
+            public void onLabelClicked() {
+                MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
+                ClientHelper.getInstance().executeViewAllRecipesKeyBind();
+            }
+            
+            @Override
+            public Optional<String> getTooltips() {
+                return Optional.ofNullable(I18n.translate("text.rei.view_all_categories"));
+            }
+            
+            @Override
+            public void render(int mouseX, int mouseY, float delta) {
+                GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
+                int colour = getDefaultColor();
+                if (clickable && isHovered(mouseX, mouseY))
+                    colour = getHoveredColor();
+                font.draw((isHovered(mouseX, mouseY) ? "§n" : "") + text, x - font.getStringWidth(text) / 2, y, colour);
+                if (clickable && getTooltips().isPresent())
+                    if (!focused && isHighlighted(mouseX, mouseY))
+                        ScreenHelper.getLastOverlay().addTooltip(QueuedTooltip.create(getTooltips().get().split("\n")));
+                    else if (focused)
+                        ScreenHelper.getLastOverlay().addTooltip(QueuedTooltip.create(new Point(x, y), getTooltips().get().split("\n")));
+            }
+    
+            @Override
+            public int getHoveredColor() {
+                return -1;
+            }
+    
+            @Override
+            public int getDefaultColor() {
+                return 4210752;
+            }
+        });
+        this.children.addAll(buttonWidgets);
+        this.widgets.addAll(tabs);
+        this.children.addAll(widgets);
+        this.children.add(ScreenHelper.getLastOverlay(true, false));
+        ScreenHelper.getLastOverlay().init();
+    }
+    
+    @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));
+        if (scrollListBounds.contains(double_1, double_2) && height > scrollListBounds.height - 2) {
+            if (double_3 > 0)
+                scroll -= 16;
+            else
+                scroll += 16;
+            scroll = MathHelper.clamp(scroll, 0, height - scrollListBounds.height + 2);
+            return true;
+        }
+        return super.mouseScrolled(double_1, double_2, double_3);
+    }
+    
+    @Override
+    public void render(int mouseX, int mouseY, float delta) {
+        this.fillGradient(0, 0, this.width, this.height, -1072689136, -804253680);
+        int yOffset = 0;
+        this.widgets.forEach(widget -> {
+            GuiLighting.disable();
+            widget.render(mouseX, mouseY, delta);
+        });
+        GuiLighting.disable();
+        ScreenHelper.getLastOverlay().render(mouseX, mouseY, delta);
+        GL11.glPushMatrix();
+        Scissors.begin();
+        Scissors.scissor(0, scrollListBounds.y + 1, width, scrollListBounds.height - 2);
+        for(int i = 0; i < buttonWidgets.size(); i++) {
+            ButtonWidget buttonWidget = buttonWidgets.get(i);
+            buttonWidget.getBounds().y = scrollListBounds.y + 1 + yOffset - (int) scroll;
+            if (buttonWidget.getBounds().getMaxY() > scrollListBounds.getMinY() && buttonWidget.getBounds().getMinY() < scrollListBounds.getMaxY()) {
+                GuiLighting.disable();
+                buttonWidget.render(mouseX, mouseY, delta);
+            }
+            yOffset += buttonWidget.getBounds().height;
+        }
+        for(int i = 0; i < buttonWidgets.size(); i++) {
+            if (buttonWidgets.get(i).getBounds().getMaxY() > scrollListBounds.getMinY() && buttonWidgets.get(i).getBounds().getMinY() < scrollListBounds.getMaxY()) {
+                GuiLighting.disable();
+                recipeRenderers.get(i).setBlitOffset(1);
+                recipeRenderers.get(i).render(buttonWidgets.get(i).getBounds().x, buttonWidgets.get(i).getBounds().y, mouseX, mouseY, delta);
+            }
+        }
+        Scissors.end();
+        GL11.glPopMatrix();
+        ScreenHelper.getLastOverlay().lateRender(mouseX, mouseY, delta);
+    }
+    
+    private int getTitleBarHeight() {
+        IntBuffer useless = BufferUtils.createIntBuffer(3), top = BufferUtils.createIntBuffer(1);
+        GLFW.glfwGetWindowFrameSize(minecraft.window.getHandle(), useless, top, useless, useless);
+        System.out.println(top.get(0));
+        return top.get(0) / 3 * 2;
+    }
+    
+    private int getReal(int i) {
+        return (int) (i / ((double) minecraft.window.getScaledWidth() / (double) minecraft.window.getWidth()));
+    }
+    
+    @Override
+    public boolean keyPressed(int int_1, int int_2, int int_3) {
+        if ((int_1 == 256 || this.minecraft.options.keyInventory.matchesKey(int_1, int_2)) && this.shouldCloseOnEsc()) {
+            MinecraftClient.getInstance().openScreen(ScreenHelper.getLastContainerScreen());
+            ScreenHelper.getLastOverlay().init();
+            return true;
+        }
+        if (int_1 == 258) {
+            boolean boolean_1 = !hasShiftDown();
+            if (!this.changeFocus(boolean_1))
+                this.changeFocus(boolean_1);
+            return true;
+        }
+        if (ClientHelper.getInstance().getNextPageKeyBinding().matchesKey(int_1, int_2)) {
+            if (categoryMap.get(categories.get(selectedCategoryIndex)).size() > 1) {
+                selectedCategoryIndex++;
+                if (selectedCategoryIndex >= categoryMap.get(categories.get(selectedCategoryIndex)).size())
+                    selectedCategoryIndex = 0;
+                init();
+                return true;
+            }
+            return false;
+        } else if (ClientHelper.getInstance().getPreviousPageKeyBinding().matchesKey(int_1, int_2)) {
+            if (categoryMap.get(categories.get(selectedCategoryIndex)).size() > 1) {
+                selectedCategoryIndex--;
+                if (selectedCategoryIndex < 0)
+                    selectedCategoryIndex = categoryMap.get(categories.get(selectedCategoryIndex)).size() - 1;
+                init();
+                return true;
+            }
+            return false;
+        }
+        for(Element element : children())
+            if (element.keyPressed(int_1, int_2, int_3))
+                return true;
+        return super.keyPressed(int_1, int_2, int_3);
+    }
+    
+}

+ 13 - 0
src/main/java/me/shedaniel/rei/gui/renderables/EmptyRenderer.java

@@ -0,0 +1,13 @@
+package me.shedaniel.rei.gui.renderables;
+
+import me.shedaniel.rei.api.Renderer;
+
+public class EmptyRenderer extends Renderer {
+    
+    public static final EmptyRenderer INSTANCE = new EmptyRenderer();
+    
+    @Override
+    public void render(int x, int y, double mouseX, double mouseY, float delta) {
+    
+    }
+}

+ 69 - 0
src/main/java/me/shedaniel/rei/gui/renderables/ItemStackRenderer.java

@@ -0,0 +1,69 @@
+package me.shedaniel.rei.gui.renderables;
+
+import com.google.common.collect.Lists;
+import com.mojang.blaze3d.platform.GlStateManager;
+import me.shedaniel.rei.api.ClientHelper;
+import me.shedaniel.rei.api.Renderer;
+import me.shedaniel.rei.client.ScreenHelper;
+import me.shedaniel.rei.gui.widget.ItemListOverlay;
+import me.shedaniel.rei.gui.widget.QueuedTooltip;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.GuiLighting;
+import net.minecraft.client.render.item.ItemRenderer;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.Identifier;
+
+import java.util.Collections;
+import java.util.List;
+
+public abstract class ItemStackRenderer extends Renderer {
+    
+    public static final Identifier CHEST_GUI_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
+    public boolean drawTooltip = false;
+    
+    @Override
+    public void render(int x, int y, double mouseX, double mouseY, float delta) {
+        int l = x - 8, i1 = y - 6;
+        GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
+        ItemRenderer itemRenderer = MinecraftClient.getInstance().getItemRenderer();
+        itemRenderer.zOffset = blitOffset;
+        GuiLighting.enableForItems();
+        GlStateManager.colorMask(true, true, true, true);
+        GlStateManager.enableLighting();
+        GlStateManager.enableRescaleNormal();
+        GlStateManager.enableDepthTest();
+        itemRenderer.renderGuiItem(getItemStack(), l, i1);
+        itemRenderer.renderGuiItemOverlay(MinecraftClient.getInstance().textRenderer, getItemStack(), l, i1);
+        itemRenderer.zOffset = 0.0F;
+        this.blitOffset = 0;
+        if (drawTooltip && mouseX >= x - 8 && mouseX <= x + 8 && mouseY >= y - 6 && mouseY <= y + 10)
+            queueTooltip(getItemStack(), delta);
+        this.drawTooltip = false;
+    }
+    
+    protected void queueTooltip(ItemStack itemStack, float delta) {
+        ScreenHelper.getLastOverlay().addTooltip(QueuedTooltip.create(getTooltip(itemStack)));
+    }
+    
+    protected List<String> getTooltip(ItemStack itemStack) {
+        final String modString = ClientHelper.getInstance().getFormattedModFromItem(itemStack.getItem());
+        List<String> toolTip = Lists.newArrayList(ItemListOverlay.tryGetItemStackToolTip(itemStack, true));
+        toolTip.addAll(getExtraToolTips(itemStack));
+        boolean alreadyHasMod = false;
+        for(String s : toolTip)
+            if (s.equalsIgnoreCase(modString)) {
+                alreadyHasMod = true;
+                break;
+            }
+        if (!alreadyHasMod)
+            toolTip.add(modString);
+        return toolTip;
+    }
+    
+    protected List<String> getExtraToolTips(ItemStack stack) {
+        return Collections.emptyList();
+    }
+    
+    public abstract ItemStack getItemStack();
+    
+}

+ 13 - 0
src/main/java/me/shedaniel/rei/gui/renderables/RecipeRenderer.java

@@ -0,0 +1,13 @@
+package me.shedaniel.rei.gui.renderables;
+
+import me.shedaniel.rei.api.Renderer;
+
+public abstract class RecipeRenderer extends Renderer {
+    
+    public abstract int getHeight();
+    
+    public final int getWidth() {
+        return 100;
+    }
+    
+}

+ 87 - 0
src/main/java/me/shedaniel/rei/gui/renderables/SimpleRecipeRenderer.java

@@ -0,0 +1,87 @@
+package me.shedaniel.rei.gui.renderables;
+
+import com.google.common.collect.Lists;
+import me.shedaniel.rei.api.Renderable;
+import me.shedaniel.rei.gui.VillagerRecipeViewingScreen;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.GuiLighting;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.Pair;
+import net.minecraft.util.math.MathHelper;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+public class SimpleRecipeRenderer extends RecipeRenderer {
+    
+    private static final Identifier CHEST_GUI_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
+    private List<ItemStackRenderer> inputRenderer;
+    private ItemStackRenderer outputRenderer;
+    
+    public SimpleRecipeRenderer(Supplier<List<List<ItemStack>>> input, Supplier<List<ItemStack>> output) {
+        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();
+            if (any.isPresent()) {
+                any.get().getRight().addAndGet(pair.getRight());
+            } else
+                newList.add(new Pair<>(pair.getLeft(), new AtomicInteger(pair.getRight())));
+        }
+        List<List<ItemStack>> b = Lists.newArrayList();
+        for(Pair<List<ItemStack>, AtomicInteger> pair : newList)
+            b.add(pair.getLeft().stream().map(stack -> {
+                ItemStack s = stack.copy();
+                s.setAmount(pair.getRight().get());
+                return s;
+            }).collect(Collectors.toList()));
+        this.inputRenderer = b.stream().filter(stacks -> !stacks.isEmpty()).map(stacks -> Renderable.fromItemStacks(stacks)).collect(Collectors.toList());
+        this.outputRenderer = Renderable.fromItemStacks(output.get().stream().filter(stack -> !stack.isEmpty()).collect(Collectors.toList()));
+    }
+    
+    @Override
+    public void render(int x, int y, double mouseX, double mouseY, float delta) {
+        int xx = x + 5, yy = y + 5;
+        int j = 0;
+        int itemsPerLine = getItemsPerLine();
+        for(ItemStackRenderer itemStackRenderer : inputRenderer) {
+            itemStackRenderer.setBlitOffset(getBlitOffset() + 50);
+            itemStackRenderer.drawTooltip = MinecraftClient.getInstance().currentScreen instanceof VillagerRecipeViewingScreen;
+            itemStackRenderer.render(xx + 8, yy + 6, mouseX, mouseY, delta);
+            xx += 18;
+            j++;
+            if (j >= getItemsPerLine() - 3) {
+                yy += 18;
+                xx = x + 5;
+                j = 0;
+            }
+        }
+        xx = x + 5 + 18 * (getItemsPerLine() - 3);
+        yy = y + getHeight() / 2 - 8;
+        GuiLighting.disable();
+        MinecraftClient.getInstance().getTextureManager().bindTexture(CHEST_GUI_TEXTURE);
+        blit(xx, yy, 0, 28, 36, 18);
+        xx += 36;
+        outputRenderer.setBlitOffset(getBlitOffset() + 50);
+        outputRenderer.drawTooltip = MinecraftClient.getInstance().currentScreen instanceof VillagerRecipeViewingScreen;
+        outputRenderer.render(xx + 8, yy + 6, mouseX, mouseY, delta);
+    }
+    
+    @Override
+    public int getHeight() {
+        return 10 + getItemsHeight() * 18;
+    }
+    
+    public int getItemsHeight() {
+        return MathHelper.ceil(((float) inputRenderer.size()) / (getItemsPerLine() - 3));
+    }
+    
+    public int getItemsPerLine() {
+        return MathHelper.floor((getWidth() - 10f) / 18f);
+    }
+    
+}

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

@@ -74,8 +74,8 @@ public abstract class ButtonWidget extends HighlightableWidget {
         this.blit(x + 4, y + height - 4, 4, 62 + textureOffset * 20, width - 8, 4);
         
         for(int i = y + 4; i < y + height - 4; i += 4) {
-            this.blit(x, i, 0, 50 + textureOffset * 20, width / 2, MathHelper.clamp(y + height - 4 - i, 0, 4));
-            this.blit(x + width / 2, i, 200 - width / 2, 50 + textureOffset * 20, width / 2, MathHelper.clamp(y + height - 4 - i, 0, 4));
+            this.blit(x, i, 0, 50 + textureOffset * 20, MathHelper.ceil(width / 2f), MathHelper.clamp(y + height - 4 - i, 0, 4));
+            this.blit(x + MathHelper.ceil(width / 2f), i, 200 - MathHelper.floor(width / 2f), 50 + textureOffset * 20, MathHelper.floor(width / 2f), MathHelper.clamp(y + height - 4 - i, 0, 4));
         }
         
         int colour = 14737632;

+ 5 - 0
src/main/java/me/shedaniel/rei/gui/widget/CategoryBaseWidget.java

@@ -13,4 +13,9 @@ public class CategoryBaseWidget extends RecipeBaseWidget {
         return 66;
     }
     
+    @Override
+    protected boolean isRendering() {
+        return true;
+    }
+    
 }

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

@@ -22,9 +22,9 @@ public abstract class ClickableLabelWidget extends LabelWidget {
     
     @Override
     public void render(int mouseX, int mouseY, float delta) {
-        int colour = -1;
+        int colour = getDefaultColor();
         if (clickable && isHovered(mouseX, mouseY))
-            colour = hoveredColor;
+            colour = getHoveredColor();
         drawCenteredString(font, (isHovered(mouseX, mouseY) ? "§n" : "") + text, x, y, colour);
         if (clickable && getTooltips().isPresent())
             if (!focused && isHighlighted(mouseX, mouseY))
@@ -33,6 +33,14 @@ public abstract class ClickableLabelWidget extends LabelWidget {
                 ScreenHelper.getLastOverlay().addTooltip(QueuedTooltip.create(new Point(x, y), getTooltips().get().split("\n")));
     }
     
+    public int getDefaultColor() {
+        return -1;
+    }
+    
+    public int getHoveredColor() {
+        return hoveredColor;
+    }
+    
     @Override
     public boolean mouseClicked(double mouseX, double mouseY, int button) {
         if (button == 0 && clickable && isHighlighted(mouseX, mouseY)) {

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

@@ -108,7 +108,7 @@ public class ItemListOverlay extends Widget {
                 j++;
                 if (j > currentDisplayed.size())
                     break;
-                widgets.add(new ItemSlotWidget(x, y, Collections.singletonList(currentDisplayed.get(j - 1)), false, true, true) {
+                widgets.add(new SlotWidget(x, y, Collections.singletonList(currentDisplayed.get(j - 1)), false, true, true) {
                     @Override
                     protected void queueTooltip(ItemStack itemStack, float delta) {
                         ClientPlayerEntity player = minecraft.player;
@@ -118,10 +118,10 @@ public class ItemListOverlay extends Widget {
                     
                     @Override
                     public boolean mouseClicked(double mouseX, double mouseY, int button) {
-                        if (isHighlighted(mouseX, mouseY)) {
+                        if (isCurrentRendererItem() && isHighlighted(mouseX, mouseY)) {
                             if (ClientHelper.getInstance().isCheating()) {
-                                if (getCurrentStack() != null && !getCurrentStack().isEmpty()) {
-                                    ItemStack cheatedStack = getCurrentStack().copy();
+                                if (getCurrentItemStack() != null && !getCurrentItemStack().isEmpty()) {
+                                    ItemStack cheatedStack = getCurrentItemStack().copy();
                                     if (RoughlyEnoughItemsCore.getConfigManager().getConfig().itemCheatingMode == ItemCheatingMode.REI_LIKE)
                                         cheatedStack.setAmount(button != 1 ? 1 : cheatedStack.getMaxAmount());
                                     else if (RoughlyEnoughItemsCore.getConfigManager().getConfig().itemCheatingMode == ItemCheatingMode.JEI_LIKE)
@@ -131,9 +131,9 @@ public class ItemListOverlay extends Widget {
                                     return ClientHelper.getInstance().tryCheatingStack(cheatedStack);
                                 }
                             } else if (button == 0)
-                                return ClientHelper.getInstance().executeRecipeKeyBind(getCurrentStack().copy());
+                                return ClientHelper.getInstance().executeRecipeKeyBind(getCurrentItemStack().copy());
                             else if (button == 1)
-                                return ClientHelper.getInstance().executeUsageKeyBind(getCurrentStack().copy());
+                                return ClientHelper.getInstance().executeUsageKeyBind(getCurrentItemStack().copy());
                         }
                         return false;
                     }

+ 10 - 143
src/main/java/me/shedaniel/rei/gui/widget/ItemSlotWidget.java

@@ -1,158 +1,25 @@
 package me.shedaniel.rei.gui.widget;
 
-import com.google.common.collect.Lists;
-import com.mojang.blaze3d.platform.GlStateManager;
-import me.shedaniel.cloth.api.ClientUtils;
-import me.shedaniel.rei.RoughlyEnoughItemsCore;
-import me.shedaniel.rei.api.ClientHelper;
-import me.shedaniel.rei.client.ScreenHelper;
-import net.minecraft.client.gui.Element;
-import net.minecraft.client.render.GuiLighting;
-import net.minecraft.client.render.item.ItemRenderer;
+import me.shedaniel.rei.api.Renderer;
 import net.minecraft.item.ItemStack;
-import net.minecraft.item.Items;
-import net.minecraft.util.Identifier;
-import net.minecraft.util.math.MathHelper;
 
-import java.awt.*;
-import java.util.Collections;
-import java.util.LinkedList;
+import java.util.Collection;
 import java.util.List;
 
-public class ItemSlotWidget extends HighlightableWidget {
-    
-    private static final Identifier RECIPE_GUI = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
-    private List<ItemStack> itemList = new LinkedList<>();
-    private boolean drawBackground, showToolTips, clickToMoreRecipes, drawHighlightedBackground;
-    private int x, y;
-    
+public class ItemSlotWidget extends SlotWidget {
     public ItemSlotWidget(int x, int y, ItemStack itemStack, boolean drawBackground, boolean showToolTips) {
-        this(x, y, Collections.singletonList(itemStack), drawBackground, showToolTips);
-    }
-    
-    public ItemSlotWidget(int x, int y, List<ItemStack> itemList, boolean drawBackground, boolean showToolTips) {
-        this.itemList = itemList;
-        this.drawBackground = drawBackground;
-        this.showToolTips = showToolTips;
-        this.x = x;
-        this.y = y;
-        this.clickToMoreRecipes = false;
-        this.drawHighlightedBackground = true;
-    }
-    
-    public ItemSlotWidget(int x, int y, List<ItemStack> itemList, boolean drawBackground, boolean showToolTips, boolean clickToMoreRecipes) {
-        this(x, y, itemList, drawBackground, showToolTips);
-        this.clickToMoreRecipes = clickToMoreRecipes;
-    }
-    
-    @Override
-    public List<? extends Element> children() {
-        return Collections.emptyList();
-    }
-    
-    public void setDrawHighlightedBackground(boolean drawHighlightedBackground) {
-        this.drawHighlightedBackground = drawHighlightedBackground;
-    }
-    
-    public boolean isDrawBackground() {
-        return drawBackground;
-    }
-    
-    @Override
-    public void render(int mouseX, int mouseY, float delta) {
-        ItemStack itemStack = getCurrentStack().copy();
-        if (drawBackground) {
-            minecraft.getTextureManager().bindTexture(RECIPE_GUI);
-            blit(this.x - 1, this.y - 1, 0, 222, 18, 18);
-        }
-        boolean highlighted = isHighlighted(mouseX, mouseY);
-        if (drawHighlightedBackground && highlighted) {
-            GlStateManager.disableLighting();
-            GlStateManager.disableDepthTest();
-            GlStateManager.colorMask(true, true, true, false);
-            fillGradient(x, y, x + 16, y + 16, -2130706433, -2130706433);
-            GlStateManager.colorMask(true, true, true, true);
-            GlStateManager.enableLighting();
-            GlStateManager.enableDepthTest();
-        }
-        if (!itemStack.isEmpty()) {
-            if (RoughlyEnoughItemsCore.getConfigManager().getConfig().aprilFoolsFish2019 && !highlighted)
-                itemStack = Items.TROPICAL_FISH.getDefaultStack();
-            GuiLighting.enableForItems();
-            ItemRenderer itemRenderer = minecraft.getItemRenderer();
-            itemRenderer.zOffset = 200.0F;
-            itemRenderer.renderGuiItem(itemStack, x, y);
-            itemRenderer.renderGuiItemOverlay(font, itemStack, x, y, getItemCountOverlay(itemStack));
-            itemRenderer.zOffset = 0.0F;
-        }
-        if (!itemStack.isEmpty() && highlighted && showToolTips)
-            queueTooltip(itemStack, delta);
+        super(x, y, itemStack, drawBackground, showToolTips);
     }
     
-    protected void queueTooltip(ItemStack itemStack, float delta) {
-        ScreenHelper.getLastOverlay().addTooltip(QueuedTooltip.create(getTooltip(itemStack)));
+    public ItemSlotWidget(int x, int y, Collection<ItemStack> itemList, boolean drawBackground, boolean showToolTips) {
+        super(x, y, itemList, drawBackground, showToolTips);
     }
     
-    protected List<String> getTooltip(ItemStack itemStack) {
-        final String modString = ClientHelper.getInstance().getFormattedModFromItem(itemStack.getItem());
-        List<String> toolTip = Lists.newArrayList(ItemListOverlay.tryGetItemStackToolTip(itemStack, true));
-        toolTip.addAll(getExtraToolTips(itemStack));
-        boolean alreadyHasMod = false;
-        for(String s : toolTip)
-            if (s.equalsIgnoreCase(modString)) {
-                alreadyHasMod = true;
-                break;
-            }
-        if (!alreadyHasMod)
-            toolTip.add(modString);
-        return toolTip;
+    public ItemSlotWidget(int x, int y, List<Renderer> renderers, boolean drawBackground, boolean showToolTips) {
+        super(x, y, renderers, drawBackground, showToolTips);
     }
     
-    protected List<String> getExtraToolTips(ItemStack stack) {
-        return Collections.emptyList();
-    }
-    
-    protected String getItemCountOverlay(ItemStack currentStack) {
-        return "";
-    }
-    
-    public ItemStack getCurrentStack() {
-        if (itemList.size() == 0)
-            return new ItemStack(Items.AIR);
-        return itemList.get(MathHelper.floor((System.currentTimeMillis() / 500 % (double) itemList.size()) / 1f));
-    }
-    
-    public void setItemList(List<ItemStack> itemList) {
-        this.itemList = itemList;
-    }
-    
-    @Override
-    public Rectangle getBounds() {
-        return new Rectangle(this.x - 1, this.y - 1, 18, 18);
-    }
-    
-    @Override
-    public boolean mouseClicked(double mouseX, double mouseY, int button) {
-        if (!clickToMoreRecipes)
-            return false;
-        if (getBounds().contains(mouseX, mouseY))
-            if (button == 0)
-                return ClientHelper.getInstance().executeRecipeKeyBind(getCurrentStack());
-            else if (button == 1)
-                return ClientHelper.getInstance().executeUsageKeyBind(getCurrentStack());
-        return false;
-    }
-    
-    @Override
-    public boolean keyPressed(int int_1, int int_2, int int_3) {
-        if (!clickToMoreRecipes)
-            return false;
-        if (getBounds().contains(ClientUtils.getMouseLocation()))
-            if (ClientHelper.getInstance().getRecipeKeyBinding().matchesKey(int_1, int_2))
-                return ClientHelper.getInstance().executeRecipeKeyBind(getCurrentStack());
-            else if (ClientHelper.getInstance().getUsageKeyBinding().matchesKey(int_1, int_2))
-                return ClientHelper.getInstance().executeUsageKeyBind(getCurrentStack());
-        return false;
+    public ItemSlotWidget(int x, int y, List<ItemStack> itemList, boolean drawBackground, boolean showToolTips, boolean clickToMoreRecipes) {
+        super(x, y, itemList, drawBackground, showToolTips, clickToMoreRecipes);
     }
-    
 }

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

@@ -2,6 +2,7 @@ package me.shedaniel.rei.gui.widget;
 
 import com.mojang.blaze3d.platform.GlStateManager;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
+import me.shedaniel.rei.client.RecipeScreenType;
 import net.minecraft.client.render.GuiLighting;
 import net.minecraft.util.Identifier;
 
@@ -12,7 +13,6 @@ 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 Color INNER_COLOR = new Color(198, 198, 198);
     
     private Rectangle bounds;
     
@@ -38,6 +38,8 @@ public class RecipeBaseWidget extends HighlightableWidget {
     
     @Override
     public void render(int mouseX, int mouseY, float delta) {
+        if (!isRendering())
+            return;
         GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
         GuiLighting.disable();
         minecraft.getTextureManager().bindTexture(CHEST_GUI_TEXTURE);
@@ -61,7 +63,15 @@ public class RecipeBaseWidget extends HighlightableWidget {
             this.blit(x, y + yy, 106, 128 + textureOffset, 4, thisHeight);
             this.blit(x + width - 4, y + yy, 252, 128 + textureOffset, 4, thisHeight);
         }
-        fillGradient(x + 4, y + 4, x + width - 4, y + height - 4, INNER_COLOR.getRGB(), INNER_COLOR.getRGB());
+        fillGradient(x + 4, y + 4, x + width - 4, y + height - 4, getInnerColor(), getInnerColor());
+    }
+    
+    protected boolean isRendering() {
+        return RoughlyEnoughItemsCore.getConfigManager().getConfig().screenType != RecipeScreenType.VILLAGER;
+    }
+    
+    protected int getInnerColor() {
+        return -3750202;
     }
     
     protected int getTextureOffset() {

+ 26 - 0
src/main/java/me/shedaniel/rei/gui/widget/SlotBaseWidget.java

@@ -0,0 +1,26 @@
+package me.shedaniel.rei.gui.widget;
+
+import java.awt.*;
+
+public class SlotBaseWidget extends RecipeBaseWidget {
+    
+    public SlotBaseWidget(Rectangle bounds) {
+        super(bounds);
+    }
+    
+    @Override
+    public int getInnerColor() {
+        return -7631989;
+    }
+    
+    @Override
+    protected int getTextureOffset() {
+        return -66;
+    }
+    
+    @Override
+    protected boolean isRendering() {
+        return true;
+    }
+    
+}

+ 203 - 0
src/main/java/me/shedaniel/rei/gui/widget/SlotWidget.java

@@ -0,0 +1,203 @@
+package me.shedaniel.rei.gui.widget;
+
+import com.google.common.collect.Lists;
+import com.mojang.blaze3d.platform.GlStateManager;
+import me.shedaniel.cloth.api.ClientUtils;
+import me.shedaniel.rei.RoughlyEnoughItemsCore;
+import me.shedaniel.rei.api.ClientHelper;
+import me.shedaniel.rei.api.Renderable;
+import me.shedaniel.rei.api.Renderer;
+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;
+
+import java.awt.*;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class SlotWidget extends HighlightableWidget {
+    
+    private static final Identifier RECIPE_GUI = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
+    private List<Renderer> renderers = new LinkedList<>();
+    private boolean drawBackground, showToolTips, clickToMoreRecipes, drawHighlightedBackground;
+    private int x, y;
+    private static final ItemStackRenderer TROPICAL_FISH_RENDERABLE = Renderable.fromItemStack(Items.TROPICAL_FISH.getDefaultStack());
+    
+    public SlotWidget(int x, int y, ItemStack itemStack, boolean drawBackground, boolean showToolTips) {
+        this(x, y, Collections.singletonList(itemStack), drawBackground, showToolTips);
+    }
+    
+    public SlotWidget(int x, int y, Collection<ItemStack> itemList, boolean drawBackground, boolean showToolTips) {
+        this(x, y, itemList.stream().map(Renderable::fromItemStack).collect(Collectors.toList()), drawBackground, showToolTips);
+    }
+    
+    public SlotWidget(int x, int y, List<Renderer> renderers, boolean drawBackground, boolean showToolTips) {
+        this.renderers = renderers;
+        this.drawBackground = drawBackground;
+        this.showToolTips = showToolTips;
+        this.x = x;
+        this.y = y;
+        this.clickToMoreRecipes = false;
+        this.drawHighlightedBackground = true;
+    }
+    
+    public SlotWidget(int x, int y, List<ItemStack> itemList, boolean drawBackground, boolean showToolTips, boolean clickToMoreRecipes) {
+        this(x, y, itemList, drawBackground, showToolTips);
+        this.clickToMoreRecipes = clickToMoreRecipes;
+    }
+    
+    public boolean isShowToolTips() {
+        return showToolTips;
+    }
+    
+    public void setShowToolTips(boolean showToolTips) {
+        this.showToolTips = showToolTips;
+    }
+    
+    public boolean isClickToMoreRecipes() {
+        return clickToMoreRecipes;
+    }
+    
+    public void setClickToMoreRecipes(boolean clickToMoreRecipes) {
+        this.clickToMoreRecipes = clickToMoreRecipes;
+    }
+    
+    public boolean isDrawHighlightedBackground() {
+        return drawHighlightedBackground;
+    }
+    
+    public void setDrawHighlightedBackground(boolean drawHighlightedBackground) {
+        this.drawHighlightedBackground = drawHighlightedBackground;
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        return Collections.emptyList();
+    }
+    
+    public boolean isDrawBackground() {
+        return drawBackground;
+    }
+    
+    public void setDrawBackground(boolean drawBackground) {
+        this.drawBackground = drawBackground;
+    }
+    
+    @Override
+    public void render(int mouseX, int mouseY, float delta) {
+        Renderer renderer = getCurrentRenderer();
+        if (drawBackground) {
+            minecraft.getTextureManager().bindTexture(RECIPE_GUI);
+            blit(this.x - 1, this.y - 1, 0, 222, 18, 18);
+        }
+        boolean highlighted = isHighlighted(mouseX, mouseY);
+        if (drawHighlightedBackground && highlighted) {
+            GlStateManager.disableLighting();
+            GlStateManager.disableDepthTest();
+            GlStateManager.colorMask(true, true, true, false);
+            fillGradient(x, y, x + 16, y + 16, -2130706433, -2130706433);
+            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)
+                queueTooltip(getCurrentItemStack(), delta);
+        } else {
+            renderer.setBlitOffset(200);
+            renderer.render(x + 8, y + 6, mouseX, mouseY, delta);
+        }
+    }
+    
+    protected void queueTooltip(ItemStack itemStack, float delta) {
+        ScreenHelper.getLastOverlay().addTooltip(QueuedTooltip.create(getTooltip(itemStack)));
+    }
+    
+    protected List<String> getTooltip(ItemStack itemStack) {
+        final String modString = ClientHelper.getInstance().getFormattedModFromItem(itemStack.getItem());
+        List<String> toolTip = Lists.newArrayList(ItemListOverlay.tryGetItemStackToolTip(itemStack, true));
+        toolTip.addAll(getExtraToolTips(itemStack));
+        boolean alreadyHasMod = false;
+        for(String s : toolTip)
+            if (s.equalsIgnoreCase(modString)) {
+                alreadyHasMod = true;
+                break;
+            }
+        if (!alreadyHasMod)
+            toolTip.add(modString);
+        return toolTip;
+    }
+    
+    protected List<String> getExtraToolTips(ItemStack stack) {
+        return Collections.emptyList();
+    }
+    
+    protected String getItemCountOverlay(ItemStack currentStack) {
+        return "";
+    }
+    
+    public ItemStack getCurrentItemStack() {
+        if (getCurrentRenderer() instanceof ItemStackRenderer)
+            return ((ItemStackRenderer) getCurrentRenderer()).getItemStack();
+        return ItemStack.EMPTY;
+    }
+    
+    public Renderer getCurrentRenderer() {
+        if (renderers.size() == 0)
+            return Renderable.empty();
+        return renderers.get(MathHelper.floor((System.currentTimeMillis() / 500 % (double) renderers.size()) / 1f));
+    }
+    
+    public void setItemList(List<ItemStack> itemList) {
+        this.setRenderers(itemList.stream().map(Renderable::fromItemStack).collect(Collectors.toList()));
+    }
+    
+    public void setRenderers(List<Renderer> renderers) {
+        this.renderers = renderers;
+    }
+    
+    @Override
+    public Rectangle getBounds() {
+        return new Rectangle(this.x - 1, this.y - 1, 18, 18);
+    }
+    
+    @Override
+    public boolean mouseClicked(double mouseX, double mouseY, int button) {
+        if (!clickToMoreRecipes)
+            return false;
+        if (isCurrentRendererItem() && getBounds().contains(mouseX, mouseY))
+            if (button == 0)
+                return ClientHelper.getInstance().executeRecipeKeyBind(getCurrentItemStack());
+            else if (button == 1)
+                return ClientHelper.getInstance().executeUsageKeyBind(getCurrentItemStack());
+        return false;
+    }
+    
+    public boolean isCurrentRendererItem() {
+        return getCurrentRenderer() instanceof ItemStackRenderer;
+    }
+    
+    @Override
+    public boolean keyPressed(int int_1, int int_2, int int_3) {
+        if (!clickToMoreRecipes)
+            return false;
+        if (isCurrentRendererItem() && getBounds().contains(ClientUtils.getMouseLocation()))
+            if (ClientHelper.getInstance().getRecipeKeyBinding().matchesKey(int_1, int_2))
+                return ClientHelper.getInstance().executeRecipeKeyBind(getCurrentItemStack());
+            else if (ClientHelper.getInstance().getUsageKeyBinding().matchesKey(int_1, int_2))
+                return ClientHelper.getInstance().executeUsageKeyBind(getCurrentItemStack());
+        return false;
+    }
+    
+}

+ 19 - 24
src/main/java/me/shedaniel/rei/gui/widget/TabWidget.java

@@ -1,11 +1,12 @@
 package me.shedaniel.rei.gui.widget;
 
 import com.mojang.blaze3d.platform.GlStateManager;
+import me.shedaniel.rei.api.ClientHelper;
+import me.shedaniel.rei.api.RecipeCategory;
+import me.shedaniel.rei.api.Renderer;
 import me.shedaniel.rei.client.ScreenHelper;
 import me.shedaniel.rei.gui.RecipeViewingScreen;
 import net.minecraft.client.render.GuiLighting;
-import net.minecraft.client.render.item.ItemRenderer;
-import net.minecraft.item.ItemStack;
 import net.minecraft.util.Identifier;
 
 import java.awt.*;
@@ -17,28 +18,26 @@ public class TabWidget extends HighlightableWidget {
     public static final Identifier CHEST_GUI_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
     
     public boolean shown = false, selected = false;
-    public ItemStack item;
+    public Renderer renderer;
     public int id;
-    public RecipeViewingScreen recipeViewingWidget;
     public String categoryName;
     public Rectangle bounds;
-    private ItemRenderer itemRenderer;
+    public RecipeCategory category;
     
-    public TabWidget(int id, RecipeViewingScreen recipeViewingWidget, Rectangle bounds) {
+    public TabWidget(int id, Rectangle bounds) {
         this.id = id;
-        this.recipeViewingWidget = recipeViewingWidget;
         this.bounds = bounds;
-        this.itemRenderer = minecraft.getItemRenderer();
     }
     
-    public void setItem(ItemStack item, String categoryName, boolean selected) {
-        if (item == null) {
+    public void setRenderer(RecipeCategory category, Renderer renderable, String categoryName, boolean selected) {
+        if (renderable == null) {
             shown = false;
-            this.item = null;
+            this.renderer = null;
         } else {
             shown = true;
-            this.item = item;
+            this.renderer = renderable;
         }
+        this.category = category;
         this.selected = selected;
         this.categoryName = categoryName;
     }
@@ -55,8 +54,8 @@ public class TabWidget extends HighlightableWidget {
         return shown;
     }
     
-    public ItemStack getItemStack() {
-        return item;
+    public Renderer getRenderer() {
+        return renderer;
     }
     
     @Override
@@ -67,26 +66,22 @@ public class TabWidget extends HighlightableWidget {
     @Override
     public void render(int mouseX, int mouseY, float delta) {
         if (shown) {
-            int l = (int) this.bounds.getCenterX() - 8, i1 = (int) this.bounds.getCenterY() - 6;
             GlStateManager.color4f(1.0F, 1.0F, 1.0F, 1.0F);
             GuiLighting.disable();
             minecraft.getTextureManager().bindTexture(CHEST_GUI_TEXTURE);
             this.blit(bounds.x, bounds.y + 2, selected ? 28 : 0, 192, 28, (selected ? 30 : 27));
-            this.blitOffset = 100;
-            this.itemRenderer.zOffset = 100.0F;
-            GuiLighting.enableForItems();
-            this.itemRenderer.renderGuiItem(getItemStack(), l, i1);
-            this.itemRenderer.renderGuiItemOverlay(minecraft.textRenderer, getItemStack(), l, i1);
-            GlStateManager.disableLighting();
-            this.itemRenderer.zOffset = 0.0F;
-            this.blitOffset = 0;
+            renderer.setBlitOffset(100);
+            renderer.render((int) bounds.getCenterX(), (int) bounds.getCenterY(), mouseX, mouseY, delta);
             if (isHighlighted(mouseX, mouseY))
                 drawTooltip();
         }
     }
     
     private void drawTooltip() {
-        ScreenHelper.getLastOverlay().addTooltip(QueuedTooltip.create(categoryName));
+        if (this.minecraft.options.advancedItemTooltips)
+            ScreenHelper.getLastOverlay().addTooltip(QueuedTooltip.create(categoryName, "§8" + category.getIdentifier().toString(), ClientHelper.getInstance().getFormattedModFromIdentifier(category.getIdentifier())));
+        else
+            ScreenHelper.getLastOverlay().addTooltip(QueuedTooltip.create(categoryName, ClientHelper.getInstance().getFormattedModFromIdentifier(category.getIdentifier())));
     }
     
     @Override

+ 11 - 4
src/main/java/me/shedaniel/rei/plugin/DefaultBlastingCategory.java

@@ -2,7 +2,9 @@ package me.shedaniel.rei.plugin;
 
 import com.mojang.blaze3d.platform.GlStateManager;
 import me.shedaniel.rei.api.RecipeCategory;
-import me.shedaniel.rei.gui.widget.ItemSlotWidget;
+import me.shedaniel.rei.api.Renderable;
+import me.shedaniel.rei.gui.renderables.RecipeRenderer;
+import me.shedaniel.rei.gui.widget.SlotWidget;
 import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
 import me.shedaniel.rei.gui.widget.Widget;
 import net.minecraft.block.Blocks;
@@ -38,6 +40,11 @@ public class DefaultBlastingCategory implements RecipeCategory<DefaultBlastingDi
         return I18n.translate("category.rei.blasting");
     }
     
+    @Override
+    public RecipeRenderer getSimpleRenderer(DefaultBlastingDisplay recipe) {
+        return Renderable.fromRecipe(() -> Arrays.asList(recipe.getInput().get(0)), recipe::getOutput);
+    }
+    
     @Override
     public List<Widget> setupDisplay(Supplier<DefaultBlastingDisplay> recipeDisplaySupplier, Rectangle bounds) {
         final DefaultBlastingDisplay recipeDisplay = recipeDisplaySupplier.get();
@@ -57,14 +64,14 @@ public class DefaultBlastingCategory implements RecipeCategory<DefaultBlastingDi
             }
         }));
         List<List<ItemStack>> input = recipeDisplay.getInput();
-        widgets.add(new ItemSlotWidget(startPoint.x + 1, startPoint.y + 1, input.get(0), true, true, true));
-        widgets.add(new ItemSlotWidget(startPoint.x + 1, startPoint.y + 37, recipeDisplay.getFuel(), true, true, true) {
+        widgets.add(new SlotWidget(startPoint.x + 1, startPoint.y + 1, input.get(0), true, true, true));
+        widgets.add(new SlotWidget(startPoint.x + 1, startPoint.y + 37, recipeDisplay.getFuel(), true, true, true) {
             @Override
             protected List<String> getExtraToolTips(ItemStack stack) {
                 return Arrays.asList(I18n.translate("category.rei.smelting.fuel"));
             }
         });
-        widgets.add(new ItemSlotWidget(startPoint.x + 61, startPoint.y + 19, recipeDisplay.getOutput(), false, true, true));
+        widgets.add(new SlotWidget(startPoint.x + 61, startPoint.y + 19, recipeDisplay.getOutput(), false, true, true));
         return widgets;
     }
     

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

@@ -2,7 +2,7 @@ package me.shedaniel.rei.plugin;
 
 import com.mojang.blaze3d.platform.GlStateManager;
 import me.shedaniel.rei.api.RecipeCategory;
-import me.shedaniel.rei.gui.widget.ItemSlotWidget;
+import me.shedaniel.rei.gui.widget.SlotWidget;
 import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
 import me.shedaniel.rei.gui.widget.Widget;
 import net.minecraft.block.Blocks;
@@ -55,32 +55,32 @@ public class DefaultBrewingCategory implements RecipeCategory<DefaultBrewingDisp
                 blit(startPoint.x + 44, startPoint.y + 28, 103, 163, width, 4);
             }
         }));
-        widgets.add(new ItemSlotWidget(startPoint.x + 1, startPoint.y + 1, Arrays.asList(new ItemStack(Items.BLAZE_POWDER)), false, true, true));
-        widgets.add(new ItemSlotWidget(startPoint.x + 63, startPoint.y + 1, recipeDisplay.getInput().get(0), false, true, true) {
+        widgets.add(new SlotWidget(startPoint.x + 1, startPoint.y + 1, Arrays.asList(new ItemStack(Items.BLAZE_POWDER)), false, true, true));
+        widgets.add(new SlotWidget(startPoint.x + 63, startPoint.y + 1, recipeDisplay.getInput().get(0), false, true, true) {
             @Override
             protected List<String> getExtraToolTips(ItemStack stack) {
                 return Arrays.asList(I18n.translate("category.rei.brewing.input"));
             }
         });
-        widgets.add(new ItemSlotWidget(startPoint.x + 40, startPoint.y + 1, recipeDisplay.getInput().get(1), false, true, true) {
+        widgets.add(new SlotWidget(startPoint.x + 40, startPoint.y + 1, recipeDisplay.getInput().get(1), false, true, true) {
             @Override
             protected List<String> getExtraToolTips(ItemStack stack) {
                 return Arrays.asList(I18n.translate("category.rei.brewing.reactant"));
             }
         });
-        widgets.add(new ItemSlotWidget(startPoint.x + 40, startPoint.y + 35, recipeDisplay.getOutput(0), false, true, true) {
+        widgets.add(new SlotWidget(startPoint.x + 40, startPoint.y + 35, recipeDisplay.getOutput(0), false, true, true) {
             @Override
             protected List<String> getExtraToolTips(ItemStack stack) {
                 return Arrays.asList(I18n.translate("category.rei.brewing.result"));
             }
         });
-        widgets.add(new ItemSlotWidget(startPoint.x + 63, startPoint.y + 42, recipeDisplay.getOutput(1), false, true, true) {
+        widgets.add(new SlotWidget(startPoint.x + 63, startPoint.y + 42, recipeDisplay.getOutput(1), false, true, true) {
             @Override
             protected List<String> getExtraToolTips(ItemStack stack) {
                 return Arrays.asList(I18n.translate("category.rei.brewing.result"));
             }
         });
-        widgets.add(new ItemSlotWidget(startPoint.x + 86, startPoint.y + 35, recipeDisplay.getOutput(2), false, true, true) {
+        widgets.add(new SlotWidget(startPoint.x + 86, startPoint.y + 35, recipeDisplay.getOutput(2), false, true, true) {
             @Override
             protected List<String> getExtraToolTips(ItemStack stack) {
                 return Arrays.asList(I18n.translate("category.rei.brewing.result"));

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

@@ -2,7 +2,7 @@ package me.shedaniel.rei.plugin;
 
 import com.mojang.blaze3d.platform.GlStateManager;
 import me.shedaniel.rei.api.RecipeCategory;
-import me.shedaniel.rei.gui.widget.ItemSlotWidget;
+import me.shedaniel.rei.gui.widget.SlotWidget;
 import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
 import me.shedaniel.rei.gui.widget.Widget;
 import net.minecraft.block.Blocks;
@@ -58,8 +58,8 @@ public class DefaultCampfireCategory implements RecipeCategory<DefaultCampfireDi
                 MinecraftClient.getInstance().textRenderer.draw(text, bounds.x + bounds.width - length - 5, startPoint.y + 54 - 8, 4210752);
             }
         }));
-        widgets.add(new ItemSlotWidget(startPoint.x + 1, startPoint.y + 11, recipeDisplaySupplier.get().getInput().get(0), true, true, true));
-        widgets.add(new ItemSlotWidget(startPoint.x + 61, startPoint.y + 19, recipeDisplaySupplier.get().getOutput(), false, true, true));
+        widgets.add(new SlotWidget(startPoint.x + 1, startPoint.y + 11, recipeDisplaySupplier.get().getInput().get(0), true, true, true));
+        widgets.add(new SlotWidget(startPoint.x + 61, startPoint.y + 19, recipeDisplaySupplier.get().getOutput(), false, true, true));
         return widgets;
     }
     

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

@@ -3,7 +3,7 @@ package me.shedaniel.rei.plugin;
 import com.google.common.collect.Lists;
 import com.mojang.blaze3d.platform.GlStateManager;
 import me.shedaniel.rei.api.RecipeCategory;
-import me.shedaniel.rei.gui.widget.ItemSlotWidget;
+import me.shedaniel.rei.gui.widget.SlotWidget;
 import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
 import me.shedaniel.rei.gui.widget.Widget;
 import net.minecraft.block.Blocks;
@@ -52,10 +52,10 @@ public class DefaultCraftingCategory implements RecipeCategory<DefaultCraftingDi
             }
         }));
         List<List<ItemStack>> input = recipeDisplaySupplier.get().getInput();
-        List<ItemSlotWidget> slots = Lists.newArrayList();
+        List<SlotWidget> slots = Lists.newArrayList();
         for(int y = 0; y < 3; y++)
             for(int x = 0; x < 3; x++)
-                slots.add(new ItemSlotWidget(startPoint.x + 1 + x * 18, startPoint.y + 1 + y * 18, Lists.newArrayList(), true, true, true));
+                slots.add(new SlotWidget(startPoint.x + 1 + x * 18, startPoint.y + 1 + y * 18, Lists.newArrayList(), true, true, true));
         for(int i = 0; i < input.size(); i++) {
             if (recipeDisplaySupplier.get() instanceof DefaultShapedDisplay) {
                 if (!input.get(i).isEmpty())
@@ -64,7 +64,7 @@ public class DefaultCraftingCategory implements RecipeCategory<DefaultCraftingDi
                 slots.get(i).setItemList(input.get(i));
         }
         widgets.addAll(slots);
-        widgets.add(new ItemSlotWidget(startPoint.x + 95, startPoint.y + 19, recipeDisplaySupplier.get().getOutput(), false, true, true) {
+        widgets.add(new SlotWidget(startPoint.x + 95, startPoint.y + 19, recipeDisplaySupplier.get().getOutput(), false, true, true) {
             @Override
             protected String getItemCountOverlay(ItemStack currentStack) {
                 if (currentStack.getAmount() == 1)

+ 31 - 8
src/main/java/me/shedaniel/rei/plugin/DefaultPlugin.java

@@ -5,6 +5,7 @@ import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.*;
 import me.shedaniel.rei.client.ScreenHelper;
 import me.shedaniel.rei.gui.RecipeViewingScreen;
+import me.shedaniel.rei.gui.VillagerRecipeViewingScreen;
 import me.shedaniel.rei.listeners.ContainerScreenHooks;
 import me.shedaniel.rei.listeners.RecipeBookGuiHooks;
 import net.minecraft.client.MinecraftClient;
@@ -41,13 +42,13 @@ import java.util.List;
 
 public class DefaultPlugin implements REIPluginEntry {
     
-    public static final Identifier CRAFTING = new Identifier("roughlyenoughitems", "plugins/crafting");
-    public static final Identifier SMELTING = new Identifier("roughlyenoughitems", "plugins/smelting");
-    public static final Identifier SMOKING = new Identifier("roughlyenoughitems", "plugins/smoking");
-    public static final Identifier BLASTING = new Identifier("roughlyenoughitems", "plugins/blasting");
-    public static final Identifier CAMPFIRE = new Identifier("roughlyenoughitems", "plugins/campfire");
-    public static final Identifier STONE_CUTTING = new Identifier("roughlyenoughitems", "plugins/stone_cutting");
-    public static final Identifier BREWING = new Identifier("roughlyenoughitems", "plugins/brewing");
+    public static final Identifier CRAFTING = new Identifier("minecraft", "plugins/crafting");
+    public static final Identifier SMELTING = new Identifier("minecraft", "plugins/smelting");
+    public static final Identifier SMOKING = new Identifier("minecraft", "plugins/smoking");
+    public static final Identifier BLASTING = new Identifier("minecraft", "plugins/blasting");
+    public static final Identifier CAMPFIRE = new Identifier("minecraft", "plugins/campfire");
+    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 List<DefaultBrewingDisplay> BREWING_DISPLAYS = Lists.newArrayList();
@@ -81,7 +82,7 @@ public class DefaultPlugin implements REIPluginEntry {
             }
         });
         Registry.ENCHANTMENT.forEach(enchantment -> {
-            for(int i = enchantment.getMinimumLevel(); i < enchantment.getMaximumLevel(); i++) {
+            for(int i = enchantment.getMinimumLevel(); i <= enchantment.getMaximumLevel(); i++) {
                 Map<Enchantment, Integer> map = new HashMap<>();
                 map.put(enchantment, i);
                 ItemStack itemStack = new ItemStack(Items.ENCHANTED_BOOK);
@@ -192,6 +193,28 @@ public class DefaultPlugin implements REIPluginEntry {
                 return -1.0f;
             }
         });
+        displayHelper.registerBoundsHandler(new DisplayHelper.DisplayBoundsHandler<VillagerRecipeViewingScreen>() {
+            @Override
+            public Class getBaseSupportedClass() {
+                return VillagerRecipeViewingScreen.class;
+            }
+            
+            @Override
+            public Rectangle getLeftBounds(VillagerRecipeViewingScreen screen) {
+                return new Rectangle(2, 0, ((VillagerRecipeViewingScreen) screen).bounds.x - 4, MinecraftClient.getInstance().window.getScaledHeight());
+            }
+            
+            @Override
+            public Rectangle getRightBounds(VillagerRecipeViewingScreen screen) {
+                int startX = ((VillagerRecipeViewingScreen) screen).bounds.x + ((VillagerRecipeViewingScreen) screen).bounds.width + 2;
+                return new Rectangle(startX, 0, MinecraftClient.getInstance().window.getScaledWidth() - startX - 2, MinecraftClient.getInstance().window.getScaledHeight());
+            }
+            
+            @Override
+            public float getPriority() {
+                return -1.0f;
+            }
+        });
         displayHelper.registerBoundsHandler(new DisplayHelper.DisplayBoundsHandler<CreativePlayerInventoryScreen>() {
             @Override
             public Class getBaseSupportedClass() {

+ 11 - 4
src/main/java/me/shedaniel/rei/plugin/DefaultSmeltingCategory.java

@@ -2,7 +2,9 @@ package me.shedaniel.rei.plugin;
 
 import com.mojang.blaze3d.platform.GlStateManager;
 import me.shedaniel.rei.api.RecipeCategory;
-import me.shedaniel.rei.gui.widget.ItemSlotWidget;
+import me.shedaniel.rei.api.Renderable;
+import me.shedaniel.rei.gui.renderables.RecipeRenderer;
+import me.shedaniel.rei.gui.widget.SlotWidget;
 import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
 import me.shedaniel.rei.gui.widget.Widget;
 import net.minecraft.block.Blocks;
@@ -38,6 +40,11 @@ public class DefaultSmeltingCategory implements RecipeCategory<DefaultSmeltingDi
         return I18n.translate("category.rei.smelting");
     }
     
+    @Override
+    public RecipeRenderer getSimpleRenderer(DefaultSmeltingDisplay recipe) {
+        return Renderable.fromRecipe(() -> Arrays.asList(recipe.getInput().get(0)), recipe::getOutput);
+    }
+    
     @Override
     public List<Widget> setupDisplay(Supplier<DefaultSmeltingDisplay> recipeDisplaySupplier, Rectangle bounds) {
         Point startPoint = new Point((int) bounds.getCenterX() - 41, (int) bounds.getCenterY() - 27);
@@ -56,14 +63,14 @@ public class DefaultSmeltingCategory implements RecipeCategory<DefaultSmeltingDi
             }
         }));
         List<List<ItemStack>> input = recipeDisplaySupplier.get().getInput();
-        widgets.add(new ItemSlotWidget(startPoint.x + 1, startPoint.y + 1, input.get(0), true, true, true));
-        widgets.add(new ItemSlotWidget(startPoint.x + 1, startPoint.y + 37, recipeDisplaySupplier.get().getFuel(), true, true, true) {
+        widgets.add(new SlotWidget(startPoint.x + 1, startPoint.y + 1, input.get(0), true, true, true));
+        widgets.add(new SlotWidget(startPoint.x + 1, startPoint.y + 37, recipeDisplaySupplier.get().getFuel(), true, true, true) {
             @Override
             protected List<String> getExtraToolTips(ItemStack stack) {
                 return Arrays.asList(I18n.translate("category.rei.smelting.fuel"));
             }
         });
-        widgets.add(new ItemSlotWidget(startPoint.x + 61, startPoint.y + 19, recipeDisplaySupplier.get().getOutput(), false, true, true));
+        widgets.add(new SlotWidget(startPoint.x + 61, startPoint.y + 19, recipeDisplaySupplier.get().getOutput(), false, true, true));
         return widgets;
     }
     

+ 11 - 4
src/main/java/me/shedaniel/rei/plugin/DefaultSmokingCategory.java

@@ -2,7 +2,9 @@ package me.shedaniel.rei.plugin;
 
 import com.mojang.blaze3d.platform.GlStateManager;
 import me.shedaniel.rei.api.RecipeCategory;
-import me.shedaniel.rei.gui.widget.ItemSlotWidget;
+import me.shedaniel.rei.api.Renderable;
+import me.shedaniel.rei.gui.renderables.RecipeRenderer;
+import me.shedaniel.rei.gui.widget.SlotWidget;
 import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
 import me.shedaniel.rei.gui.widget.Widget;
 import net.minecraft.block.Blocks;
@@ -38,6 +40,11 @@ public class DefaultSmokingCategory implements RecipeCategory<DefaultSmokingDisp
         return I18n.translate("category.rei.smoking");
     }
     
+    @Override
+    public RecipeRenderer getSimpleRenderer(DefaultSmokingDisplay recipe) {
+        return Renderable.fromRecipe(() -> Arrays.asList(recipe.getInput().get(0)), recipe::getOutput);
+    }
+    
     @Override
     public List<Widget> setupDisplay(Supplier<DefaultSmokingDisplay> recipeDisplaySupplier, Rectangle bounds) {
         Point startPoint = new Point((int) bounds.getCenterX() - 41, (int) bounds.getCenterY() - 27);
@@ -56,14 +63,14 @@ public class DefaultSmokingCategory implements RecipeCategory<DefaultSmokingDisp
             }
         }));
         List<List<ItemStack>> input = recipeDisplaySupplier.get().getInput();
-        widgets.add(new ItemSlotWidget(startPoint.x + 1, startPoint.y + 1, input.get(0), true, true, true));
-        widgets.add(new ItemSlotWidget(startPoint.x + 1, startPoint.y + 37, recipeDisplaySupplier.get().getFuel(), true, true, true) {
+        widgets.add(new SlotWidget(startPoint.x + 1, startPoint.y + 1, input.get(0), true, true, true));
+        widgets.add(new SlotWidget(startPoint.x + 1, startPoint.y + 37, recipeDisplaySupplier.get().getFuel(), true, true, true) {
             @Override
             protected List<String> getExtraToolTips(ItemStack stack) {
                 return Arrays.asList(I18n.translate("category.rei.smelting.fuel"));
             }
         });
-        widgets.add(new ItemSlotWidget(startPoint.x + 61, startPoint.y + 19, recipeDisplaySupplier.get().getOutput(), false, true, true));
+        widgets.add(new SlotWidget(startPoint.x + 61, startPoint.y + 19, recipeDisplaySupplier.get().getOutput(), false, true, true));
         return widgets;
     }
     

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

@@ -3,7 +3,7 @@ package me.shedaniel.rei.plugin;
 import com.mojang.blaze3d.platform.GlStateManager;
 import me.shedaniel.rei.api.DisplaySettings;
 import me.shedaniel.rei.api.RecipeCategory;
-import me.shedaniel.rei.gui.widget.ItemSlotWidget;
+import me.shedaniel.rei.gui.widget.SlotWidget;
 import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
 import me.shedaniel.rei.gui.widget.Widget;
 import net.minecraft.block.Blocks;
@@ -51,8 +51,8 @@ public class DefaultStoneCuttingCategory implements RecipeCategory<DefaultStoneC
                 blit(startPoint.x, startPoint.y, 0, 221, 82, 26);
             }
         }));
-        widgets.add(new ItemSlotWidget(startPoint.x + 4, startPoint.y + 5, recipeDisplaySupplier.get().getInput().get(0), true, true, true));
-        widgets.add(new ItemSlotWidget(startPoint.x + 61, startPoint.y + 5, recipeDisplaySupplier.get().getOutput(), false, true, true));
+        widgets.add(new SlotWidget(startPoint.x + 4, startPoint.y + 5, recipeDisplaySupplier.get().getInput().get(0), true, true, true));
+        widgets.add(new SlotWidget(startPoint.x + 61, startPoint.y + 5, recipeDisplaySupplier.get().getOutput(), false, true, true));
         return widgets;
     }
     

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

@@ -8,6 +8,7 @@ import me.shedaniel.cloth.gui.entries.StringListEntry;
 import me.shedaniel.cloth.hooks.ScreenHooks;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.ItemCheatingMode;
+import me.shedaniel.rei.client.RecipeScreenType;
 import me.shedaniel.rei.gui.config.ItemListOrderingConfig;
 import me.shedaniel.rei.gui.credits.CreditsScreen;
 import net.minecraft.client.MinecraftClient;
@@ -46,6 +47,7 @@ public class ClothScreenRegistry {
             }
         });
         ConfigScreenBuilder.CategoryBuilder appearance = builder.addCategory("text.rei.config.appearance");
+        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 -> {
             RoughlyEnoughItemsCore.getConfigManager().getConfig().itemListOrdering = config.getOrdering();

+ 7 - 1
src/main/resources/assets/roughlyenoughitems/lang/en_us.json

@@ -93,6 +93,13 @@
   "text.rei.config.light_gray_recipe_border": "Light Gray Recipe Border:",
   "text.rei.config_api_failed": "You arrived here either if Cloth Config API failed or you don't have it installed!\nUpdate / Install the API and report to the bug tracker.",
   "text.rei.back": "Back",
+  "text.rei.config.recipe_screen_type": "Screen Type",
+  "text.rei.config.recipe_screen_type.unset": "Not Set",
+  "text.rei.config.recipe_screen_type.original": "Original",
+  "text.rei.config.recipe_screen_type.villager": "Villager",
+  "text.rei.select": "Select",
+  "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",
@@ -101,7 +108,6 @@
   "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",
-
   "_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."
 }

二進制
src/main/resources/assets/roughlyenoughitems/textures/gui/recipecontainer.png


二進制
src/main/resources/assets/roughlyenoughitems/textures/gui/screenshot.png


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

@@ -1,7 +1,7 @@
 {
   "schemaVersion": 1,
   "id": "roughlyenoughitems",
-  "name": "RoughlyEnoughItems",
+  "name": "Roughly Enough Items",
   "description": "To allow players to view items and recipes. Version: ${version}",
   "version": "${version}",
   "authors": [