소스 검색

new widget api

Signed-off-by: shedaniel <daniel@shedaniel.me>
shedaniel 5 년 전
부모
커밋
1ef2a2e511
43개의 변경된 파일1901개의 추가작업 그리고 295개의 파일을 삭제
  1. 1 1
      build.gradle
  2. 2 2
      gradle.properties
  3. 33 0
      src/main/java/me/shedaniel/rei/api/DrawableConsumer.java
  4. 21 3
      src/main/java/me/shedaniel/rei/api/RecipeCategory.java
  5. 78 0
      src/main/java/me/shedaniel/rei/api/widgets/Arrow.java
  6. 78 0
      src/main/java/me/shedaniel/rei/api/widgets/BurningFire.java
  7. 191 0
      src/main/java/me/shedaniel/rei/api/widgets/Label.java
  8. 86 0
      src/main/java/me/shedaniel/rei/api/widgets/Panel.java
  9. 104 0
      src/main/java/me/shedaniel/rei/api/widgets/Slot.java
  10. 142 0
      src/main/java/me/shedaniel/rei/api/widgets/Widgets.java
  11. 8 20
      src/main/java/me/shedaniel/rei/gui/ContainerScreenOverlay.java
  12. 3 3
      src/main/java/me/shedaniel/rei/gui/PreRecipeViewingScreen.java
  13. 11 18
      src/main/java/me/shedaniel/rei/gui/RecipeViewingScreen.java
  14. 5 13
      src/main/java/me/shedaniel/rei/gui/VillagerRecipeViewingScreen.java
  15. 16 2
      src/main/java/me/shedaniel/rei/gui/widget/ClickableLabelWidget.java
  16. 2 1
      src/main/java/me/shedaniel/rei/gui/widget/EntryListWidget.java
  17. 84 2
      src/main/java/me/shedaniel/rei/gui/widget/EntryWidget.java
  18. 37 3
      src/main/java/me/shedaniel/rei/gui/widget/LabelWidget.java
  19. 19 5
      src/main/java/me/shedaniel/rei/gui/widget/QueuedTooltip.java
  20. 5 0
      src/main/java/me/shedaniel/rei/gui/widget/RecipeArrowWidget.java
  21. 10 0
      src/main/java/me/shedaniel/rei/gui/widget/RecipeBaseWidget.java
  22. 10 0
      src/main/java/me/shedaniel/rei/gui/widget/SlotBaseWidget.java
  23. 1 0
      src/main/java/me/shedaniel/rei/gui/widget/Widget.java
  24. 1 1
      src/main/java/me/shedaniel/rei/impl/ConfigObjectImpl.java
  25. 78 0
      src/main/java/me/shedaniel/rei/impl/widgets/ArrowWidget.java
  26. 78 0
      src/main/java/me/shedaniel/rei/impl/widgets/BurningFireWidget.java
  27. 49 0
      src/main/java/me/shedaniel/rei/impl/widgets/DrawableWidget.java
  28. 67 0
      src/main/java/me/shedaniel/rei/impl/widgets/FillRectangleDrawableConsumer.java
  29. 268 0
      src/main/java/me/shedaniel/rei/impl/widgets/LabelWidget.java
  30. 162 0
      src/main/java/me/shedaniel/rei/impl/widgets/PanelWidget.java
  31. 75 0
      src/main/java/me/shedaniel/rei/impl/widgets/TexturedDrawableConsumer.java
  32. 27 0
      src/main/java/me/shedaniel/rei/impl/widgets/package-info.java
  33. 14 11
      src/main/java/me/shedaniel/rei/plugin/beacon/DefaultBeaconBaseCategory.java
  34. 18 24
      src/main/java/me/shedaniel/rei/plugin/brewing/DefaultBrewingCategory.java
  35. 15 29
      src/main/java/me/shedaniel/rei/plugin/campfire/DefaultCampfireCategory.java
  36. 13 19
      src/main/java/me/shedaniel/rei/plugin/composting/DefaultCompostingCategory.java
  37. 16 31
      src/main/java/me/shedaniel/rei/plugin/cooking/DefaultCookingCategory.java
  38. 16 23
      src/main/java/me/shedaniel/rei/plugin/crafting/DefaultCraftingCategory.java
  39. 19 30
      src/main/java/me/shedaniel/rei/plugin/fuel/DefaultFuelCategory.java
  40. 16 15
      src/main/java/me/shedaniel/rei/plugin/information/DefaultInformationCategory.java
  41. 11 18
      src/main/java/me/shedaniel/rei/plugin/stonecutting/DefaultStoneCuttingCategory.java
  42. 11 19
      src/main/java/me/shedaniel/rei/plugin/stripping/DefaultStrippingCategory.java
  43. 0 2
      src/main/java/me/shedaniel/rei/utils/CollectionUtils.java

+ 1 - 1
build.gradle

@@ -3,7 +3,7 @@ import net.fabricmc.loom.task.RemapJarTask
 import java.text.SimpleDateFormat
 
 plugins {
-    id 'fabric-loom' version '0.2.6-SNAPSHOT'
+    id 'fabric-loom' version '0.2.7-SNAPSHOT'
     id 'maven-publish'
     id 'net.minecrell.licenser' version '0.4.1'
     id 'com.matthewprenger.cursegradle' version '1.4.0'

+ 2 - 2
gradle.properties

@@ -1,9 +1,9 @@
-mod_version=4.0.9-unstable
+mod_version=4.0.10-unstable
 minecraft_version=20w10a
 yarn_version=20w10a+build.2
 fabricloader_version=0.7.8+build.186
 cloth_events_version=2.0.0-unstable.202003051905
-cloth_config_version=3.0.1-unstable.202003051928
+cloth_config_version=3.0.2-unstable.202003090708
 modmenu_version=1.10.1+build.30
 fabric_api=0.4.34+build.303-1.16
 autoconfig1u=1.2.4

+ 33 - 0
src/main/java/me/shedaniel/rei/api/DrawableConsumer.java

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

+ 21 - 3
src/main/java/me/shedaniel/rei/api/RecipeCategory.java

@@ -23,7 +23,7 @@
 
 package me.shedaniel.rei.api;
 
-import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.math.Rectangle;
 import me.shedaniel.rei.gui.RecipeViewingScreen;
 import me.shedaniel.rei.gui.entries.RecipeEntry;
 import me.shedaniel.rei.gui.entries.SimpleRecipeEntry;
@@ -81,12 +81,27 @@ public interface RecipeCategory<T extends RecipeDisplay> {
      * @param recipeDisplaySupplier the supplier for getting the recipe
      * @param bounds                the bounds of the display, configurable with overriding the width, height methods.
      * @return the list of widgets
+     * @deprecated use {@link #setupDisplay(RecipeDisplay, me.shedaniel.math.Rectangle)}
      */
     @ApiStatus.OverrideOnly
-    default List<Widget> setupDisplay(Supplier<T> recipeDisplaySupplier, Rectangle bounds) {
+    @ApiStatus.ScheduledForRemoval
+    @Deprecated
+    default List<Widget> setupDisplay(Supplier<T> recipeDisplaySupplier, me.shedaniel.math.api.Rectangle bounds) {
         return Collections.singletonList(new RecipeBaseWidget(bounds));
     }
     
+    /**
+     * Setup the widgets for displaying the recipe
+     *
+     * @param recipeDisplay the recipe
+     * @param bounds        the bounds of the display, configurable with overriding the width, height methods.
+     * @return the list of widgets
+     */
+    @ApiStatus.OverrideOnly
+    default List<Widget> setupDisplay(T recipeDisplay, Rectangle bounds) {
+        return setupDisplay(() -> recipeDisplay, new me.shedaniel.math.api.Rectangle(bounds));
+    }
+    
     /**
      * Draws the category background, used in {@link RecipeViewingScreen}
      *
@@ -94,9 +109,12 @@ public interface RecipeCategory<T extends RecipeDisplay> {
      * @param mouseX the x coordinates for the mouse
      * @param mouseY the y coordinates for the mouse
      * @param delta  the delta
+     * @deprecated there is no replacement for this as this is just a dumb idea, please contact me if you want to change my mind
      */
     @ApiStatus.OverrideOnly
-    default void drawCategoryBackground(Rectangle bounds, int mouseX, int mouseY, float delta) {
+    @Deprecated
+    @ApiStatus.ScheduledForRemoval
+    default void drawCategoryBackground(me.shedaniel.math.api.Rectangle bounds, int mouseX, int mouseY, float delta) {
         PanelWidget.render(bounds, -1);
         if (REIHelper.getInstance().isDarkThemeEnabled()) {
             DrawableHelper.fill(bounds.x + 17, bounds.y + 5, bounds.x + bounds.width - 17, bounds.y + 17, 0xFF404040);

+ 78 - 0
src/main/java/me/shedaniel/rei/api/widgets/Arrow.java

@@ -0,0 +1,78 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.api.widgets;
+
+import me.shedaniel.rei.gui.widget.WidgetWithBounds;
+
+public abstract class Arrow extends WidgetWithBounds {
+    public final int getX() {
+        return getBounds().getX();
+    }
+    
+    public final int getY() {
+        return getBounds().getY();
+    }
+    
+    /**
+     * Gets the animation duration in milliseconds, -1 if animation is disabled.
+     */
+    public abstract double getAnimationDuration();
+    
+    /**
+     * Sets the animation duration in milliseconds.
+     *
+     * @param animationDurationMS animation duration in milliseconds, animation is disabled when below or equals to 0
+     */
+    public abstract void setAnimationDuration(double animationDurationMS);
+    
+    /**
+     * Sets the animation duration in milliseconds.
+     *
+     * @param animationDurationMS animation duration in milliseconds, animation is disabled when below or equals to 0
+     * @return the arrow itself
+     */
+    public final Arrow animationDurationMS(double animationDurationMS) {
+        setAnimationDuration(animationDurationMS);
+        return this;
+    }
+    
+    /**
+     * Sets the animation duration in ticks.
+     *
+     * @param animationDurationTicks animation duration in ticks, animation is disabled when below or equals to 0
+     * @return the arrow itself
+     */
+    public final Arrow animationDurationTicks(double animationDurationTicks) {
+        return animationDurationMS(animationDurationTicks * 50);
+    }
+    
+    /**
+     * Disables the animation.
+     *
+     * @return the arrow itself
+     */
+    public final Arrow disableAnimation() {
+        return animationDurationMS(-1);
+    }
+}

+ 78 - 0
src/main/java/me/shedaniel/rei/api/widgets/BurningFire.java

@@ -0,0 +1,78 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.api.widgets;
+
+import me.shedaniel.rei.gui.widget.WidgetWithBounds;
+
+public abstract class BurningFire extends WidgetWithBounds {
+    public final int getX() {
+        return getBounds().getX();
+    }
+    
+    public final int getY() {
+        return getBounds().getY();
+    }
+    
+    /**
+     * Gets the animation duration in milliseconds, -1 if animation is disabled.
+     */
+    public abstract double getAnimationDuration();
+    
+    /**
+     * Sets the animation duration in milliseconds.
+     *
+     * @param animationDurationMS animation duration in milliseconds, animation is disabled when below or equals to 0
+     */
+    public abstract void setAnimationDuration(double animationDurationMS);
+    
+    /**
+     * Sets the animation duration in milliseconds.
+     *
+     * @param animationDurationMS animation duration in milliseconds, animation is disabled when below or equals to 0
+     * @return the arrow itself
+     */
+    public final BurningFire animationDurationMS(double animationDurationMS) {
+        setAnimationDuration(animationDurationMS);
+        return this;
+    }
+    
+    /**
+     * Sets the animation duration in ticks.
+     *
+     * @param animationDurationTicks animation duration in ticks, animation is disabled when below or equals to 0
+     * @return the arrow itself
+     */
+    public final BurningFire animationDurationTicks(double animationDurationTicks) {
+        return animationDurationMS(animationDurationTicks * 50);
+    }
+    
+    /**
+     * Disables the animation.
+     *
+     * @return the arrow itself
+     */
+    public final BurningFire disableAnimation() {
+        return animationDurationMS(-1);
+    }
+}

+ 191 - 0
src/main/java/me/shedaniel/rei/api/widgets/Label.java

@@ -0,0 +1,191 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.api.widgets;
+
+import me.shedaniel.math.Point;
+import me.shedaniel.rei.api.REIHelper;
+import me.shedaniel.rei.gui.widget.WidgetWithBounds;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+public abstract class Label extends WidgetWithBounds {
+    public static final int LEFT_ALIGNED = -1;
+    public static final int CENTER = 0;
+    public static final int RIGHT_ALIGNED = 1;
+    
+    public abstract boolean isClickable();
+    
+    public abstract void setClickable(boolean clickable);
+    
+    public final Label clickable() {
+        return clickable(true);
+    }
+    
+    public final Label clickable(boolean clickable) {
+        setClickable(clickable);
+        return this;
+    }
+    
+    @Nullable
+    public abstract Consumer<Label> getOnClick();
+    
+    public abstract void setOnClick(@Nullable Consumer<Label> onClick);
+    
+    public final Label onClick(@Nullable Consumer<Label> onClick) {
+        setOnClick(onClick);
+        return this;
+    }
+    
+    @Nullable
+    public abstract Consumer<Label> getOnRender();
+    
+    public abstract void setOnRender(@Nullable Consumer<Label> onRender);
+    
+    public final Label onRender(@Nullable Consumer<Label> onRender) {
+        setOnRender(onRender);
+        return this;
+    }
+    
+    public abstract boolean isFocusable();
+    
+    public abstract void setFocusable(boolean focusable);
+    
+    public final Label focusable(boolean focusable) {
+        setFocusable(focusable);
+        return this;
+    }
+    
+    @Nullable
+    public abstract String getTooltip();
+    
+    public abstract void setTooltip(@Nullable Function<Label, @Nullable String> tooltip);
+    
+    public final Label tooltipLines(@NotNull String... tooltip) {
+        return tooltipLine(String.join("\n", tooltip));
+    }
+    
+    public final Label tooltipLine(@Nullable String tooltip) {
+        return tooltipSupplier(label -> tooltip);
+    }
+    
+    public final Label tooltipSupplier(@Nullable Function<Label, @Nullable String> tooltip) {
+        setTooltip(tooltip);
+        return this;
+    }
+    
+    public abstract int getHorizontalAlignment();
+    
+    public final Label centered() {
+        return horizontalAlignment(CENTER);
+    }
+    
+    public final Label leftAligned() {
+        return horizontalAlignment(LEFT_ALIGNED);
+    }
+    
+    public final Label rightAligned() {
+        return horizontalAlignment(RIGHT_ALIGNED);
+    }
+    
+    public abstract void setHorizontalAlignment(int horizontalAlignment);
+    
+    public final Label horizontalAlignment(int horizontalAlignment) {
+        setHorizontalAlignment(horizontalAlignment);
+        return this;
+    }
+    
+    public abstract boolean hasShadow();
+    
+    public final Label noShadow() {
+        return shadow(false);
+    }
+    
+    public final Label shadow() {
+        return shadow(true);
+    }
+    
+    public abstract void setShadow(boolean hasShadow);
+    
+    public final Label shadow(boolean hasShadow) {
+        setShadow(hasShadow);
+        return this;
+    }
+    
+    public abstract int getColor();
+    
+    public abstract void setColor(int color);
+    
+    public final Label color(int lightModeColor, int darkModeColor) {
+        return color(REIHelper.getInstance().isDarkThemeEnabled() ? darkModeColor : lightModeColor);
+    }
+    
+    public final Label color(int color) {
+        setColor(color);
+        return this;
+    }
+    
+    public abstract int getHoveredColor();
+    
+    public abstract void setHoveredColor(int hoveredColor);
+    
+    public final Label hoveredColor(int lightModeColor, int darkModeColor) {
+        return hoveredColor(REIHelper.getInstance().isDarkThemeEnabled() ? darkModeColor : lightModeColor);
+    }
+    
+    public final Label hoveredColor(int color) {
+        setHoveredColor(color);
+        return this;
+    }
+    
+    @NotNull
+    public abstract Point getPoint();
+    
+    public final int getX() {
+        return getPoint().getX();
+    }
+    
+    public final int getY() {
+        return getPoint().getY();
+    }
+    
+    public abstract void setPoint(@NotNull Point point);
+    
+    public final Label point(@NotNull Point point) {
+        setPoint(point);
+        return this;
+    }
+    
+    @NotNull
+    public abstract String getText();
+    
+    public abstract void setText(@NotNull String text);
+    
+    public final Label text(@NotNull String text) {
+        setText(text);
+        return this;
+    }
+}

+ 86 - 0
src/main/java/me/shedaniel/rei/api/widgets/Panel.java

@@ -0,0 +1,86 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.api.widgets;
+
+import me.shedaniel.rei.api.REIHelper;
+import me.shedaniel.rei.gui.widget.WidgetWithBounds;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.function.Predicate;
+
+public abstract class Panel extends WidgetWithBounds {
+    public abstract int getInnerColor();
+    
+    public abstract void setInnerColor(int innerColor);
+    
+    public final Panel innerColor(int innerColor) {
+        setInnerColor(innerColor);
+        return this;
+    }
+    
+    public final Panel innerColor(int lightColor, int darkColor) {
+        return innerColor(REIHelper.getInstance().isDarkThemeEnabled() ? darkColor : lightColor);
+    }
+    
+    public abstract int getXTextureOffset();
+    
+    public abstract void setXTextureOffset(int xTextureOffset);
+    
+    public final Panel xTextureOffset(int xTextureOffset) {
+        setXTextureOffset(xTextureOffset);
+        return this;
+    }
+    
+    public abstract int getYTextureOffset();
+    
+    public abstract void setYTextureOffset(int yTextureOffset);
+    
+    public final Panel yTextureOffset(int yTextureOffset) {
+        setYTextureOffset(yTextureOffset);
+        return this;
+    }
+    
+    public abstract int getColor();
+    
+    public abstract void setColor(int color);
+    
+    public final Panel color(int color) {
+        setColor(color);
+        return this;
+    }
+    
+    public final Panel color(int lightColor, int darkColor) {
+        return color(REIHelper.getInstance().isDarkThemeEnabled() ? darkColor : lightColor);
+    }
+    
+    @NotNull
+    public abstract Predicate<Panel> getRendering();
+    
+    public abstract void setRendering(@NotNull Predicate<Panel> rendering);
+    
+    public final Panel rendering(@NotNull Predicate<Panel> rendering) {
+        setRendering(rendering);
+        return this;
+    }
+}

+ 104 - 0
src/main/java/me/shedaniel/rei/api/widgets/Slot.java

@@ -0,0 +1,104 @@
+package me.shedaniel.rei.api.widgets;
+
+import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.gui.widget.QueuedTooltip;
+import me.shedaniel.rei.gui.widget.WidgetWithBounds;
+
+import java.util.Collection;
+import java.util.List;
+
+public abstract class Slot extends WidgetWithBounds {
+    public Slot unmarkInputOrOutput() {
+        setNoticeMark((byte) 0);
+        return this;
+    }
+    
+    public final Slot markInput() {
+        setNoticeMark((byte) 1);
+        return this;
+    }
+    
+    public final Slot markOutput() {
+        setNoticeMark((byte) 2);
+        return this;
+    }
+    
+    public abstract void setNoticeMark(byte mark);
+    
+    public abstract byte getNoticeMark();
+    
+    public abstract void setInteractable(boolean interactable);
+    
+    public abstract boolean isInteractable();
+    
+    public Slot interactable(boolean interactable) {
+        setInteractable(interactable);
+        return this;
+    }
+    
+    public Slot noInteractable() {
+        return interactable(false);
+    }
+    
+    public abstract void setInteractableFavorites(boolean interactableFavorites);
+    
+    public abstract boolean isInteractableFavorites();
+    
+    public Slot interactableFavorites(boolean interactableFavorites) {
+        setInteractableFavorites(interactableFavorites);
+        return this;
+    }
+    
+    public Slot noFavoritesInteractable() {
+        return interactableFavorites(false);
+    }
+    
+    public abstract void setHighlightEnabled(boolean highlights);
+    
+    public abstract boolean isHighlightEnabled();
+    
+    public final Slot highlightEnabled(boolean highlight) {
+        setHighlightEnabled(highlight);
+        return this;
+    }
+    
+    public final Slot disableHighlight() {
+        return highlightEnabled(false);
+    }
+    
+    public abstract void setTooltipsEnabled(boolean tooltipsEnabled);
+    
+    public abstract boolean isTooltipsEnabled();
+    
+    public final Slot tooltipsEnabled(boolean tooltipsEnabled) {
+        setTooltipsEnabled(tooltipsEnabled);
+        return this;
+    }
+    
+    public final Slot disableTooltips() {
+        return tooltipsEnabled(false);
+    }
+    
+    public abstract void setBackgroundEnabled(boolean backgroundEnabled);
+    
+    public abstract boolean isBackgroundEnabled();
+    
+    public final Slot backgroundEnabled(boolean backgroundEnabled) {
+        setBackgroundEnabled(backgroundEnabled);
+        return this;
+    }
+    
+    public final Slot disableBackground() {
+        return backgroundEnabled(false);
+    }
+    
+    public abstract Slot clearEntries();
+    
+    public abstract Slot entry(EntryStack stack);
+    
+    public abstract Slot entries(Collection<EntryStack> stacks);
+    
+    public abstract List<EntryStack> getEntries();
+    
+    public abstract QueuedTooltip getCurrentTooltip(int mouseX, int mouseY);
+}

+ 142 - 0
src/main/java/me/shedaniel/rei/api/widgets/Widgets.java

@@ -0,0 +1,142 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.api.widgets;
+
+import me.shedaniel.math.Dimension;
+import me.shedaniel.math.Point;
+import me.shedaniel.math.Rectangle;
+import me.shedaniel.rei.api.ConfigObject;
+import me.shedaniel.rei.api.DrawableConsumer;
+import me.shedaniel.rei.gui.widget.EntryWidget;
+import me.shedaniel.rei.gui.widget.Widget;
+import me.shedaniel.rei.impl.widgets.*;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.sound.PositionedSoundInstance;
+import net.minecraft.sound.SoundEvents;
+import net.minecraft.util.Identifier;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.function.Consumer;
+
+public final class Widgets {
+    private Widgets() {}
+    
+    public static Widget createDrawableWidget(DrawableConsumer drawable) {
+        return new DrawableWidget(drawable);
+    }
+    
+    public static Widget createTexturedWidget(Identifier identifier, Rectangle bounds) {
+        return createTexturedWidget(identifier, bounds, 0, 0);
+    }
+    
+    public static Widget createTexturedWidget(Identifier identifier, int x, int y, int width, int height) {
+        return createTexturedWidget(identifier, x, y, 0, 0, width, height);
+    }
+    
+    public static Widget createTexturedWidget(Identifier identifier, Rectangle bounds, float u, float v) {
+        return createTexturedWidget(identifier, bounds, u, v, 256, 256);
+    }
+    
+    public static Widget createTexturedWidget(Identifier identifier, int x, int y, float u, float v, int width, int height) {
+        return createTexturedWidget(identifier, x, y, u, v, width, height, 256, 256);
+    }
+    
+    public static Widget createTexturedWidget(Identifier identifier, Rectangle bounds, float u, float v, int textureWidth, int textureHeight) {
+        return createTexturedWidget(identifier, bounds.x, bounds.y, u, v, bounds.width, bounds.height, bounds.width, bounds.height, textureWidth, textureHeight);
+    }
+    
+    public static Widget createTexturedWidget(Identifier identifier, int x, int y, float u, float v, int width, int height, int textureWidth, int textureHeight) {
+        return createTexturedWidget(identifier, x, y, u, v, width, height, width, height, textureWidth, textureHeight);
+    }
+    
+    public static Widget createTexturedWidget(Identifier identifier, Rectangle bounds, float u, float v, int uWidth, int vHeight, int textureWidth, int textureHeight) {
+        return createTexturedWidget(identifier, bounds.x, bounds.y, u, v, bounds.width, bounds.height, uWidth, vHeight, textureWidth, textureHeight);
+    }
+    
+    public static Widget createTexturedWidget(Identifier identifier, int x, int y, float u, float v, int width, int height, int uWidth, int vHeight, int textureWidth, int textureHeight) {
+        return createDrawableWidget(new TexturedDrawableConsumer(identifier, x, y, width, height, u, v, uWidth, vHeight, textureWidth, textureHeight));
+    }
+    
+    public static Widget createFilledRectangle(Rectangle rectangle, int color) {
+        return createDrawableWidget(new FillRectangleDrawableConsumer(rectangle, color));
+    }
+    
+    public static Label createLabel(Point point, @NotNull String text) {
+        return new LabelWidget(point, text);
+    }
+    
+    public static Label createClickableLabel(Point point, @NotNull String text, @Nullable Consumer<Label> onClick) {
+        return new LabelWidget(point, text).clickable().onClick(onClick);
+    }
+    
+    public static Arrow createArrow(Point point) {
+        return new ArrowWidget(new Rectangle(point, new Dimension(24, 17)));
+    }
+    
+    public static BurningFire createBurningFire(Point point) {
+        return new BurningFireWidget(new Rectangle(point, new Dimension(14, 14)));
+    }
+    
+    public static Widget createSlotBackground(Point point) {
+        return createSlotBase(new Rectangle(point.x - 1, point.y - 1, 18, 18));
+    }
+    
+    public static Widget createResultSlotBackground(Point point) {
+        return createSlotBase(new Rectangle(point.x - 5, point.y - 5, 26, 26));
+    }
+    
+    public static Panel createRecipeBase(Rectangle rectangle) {
+        return new PanelWidget(rectangle).yTextureOffset(ConfigObject.getInstance().getRecipeBorderType().getYOffset()).rendering(Widgets::shouldRecipeBaseRender);
+    }
+    
+    private static boolean shouldRecipeBaseRender(Panel panel) {
+        return ConfigObject.getInstance().getRecipeBorderType().isRendering() && PanelWidget.isRendering(panel);
+    }
+    
+    public static Panel createRecipeBase(Rectangle rectangle, int color) {
+        return createRecipeBase(rectangle).color(color);
+    }
+    
+    public static Panel createSlotBase(Rectangle rectangle) {
+        return new PanelWidget(rectangle).yTextureOffset(-66).rendering(Widgets::shouldSlotBaseRender).innerColor(-7631989, -13619152);
+    }
+    
+    private static boolean shouldSlotBaseRender(Panel panel) {
+        return true;
+    }
+    
+    public static Panel createSlotBase(Rectangle rectangle, int color) {
+        return createSlotBase(rectangle).color(color);
+    }
+    
+    @SuppressWarnings("deprecation")
+    public static Slot createSlot(Point point) {
+        return EntryWidget.create(point.x, point.y);
+    }
+    
+    public static void produceClickSound() {
+        MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
+    }
+}

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

@@ -30,6 +30,7 @@ import me.shedaniel.math.api.Rectangle;
 import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.*;
+import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.config.SearchFieldLocation;
 import me.shedaniel.rei.gui.widget.*;
 import me.shedaniel.rei.impl.ScreenHelper;
@@ -271,26 +272,13 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
             }
         }
         if (!ConfigObject.getInstance().isEntryListWidgetScrolled()) {
-            widgets.add(new ClickableLabelWidget(new Point(bounds.x + (bounds.width / 2), bounds.y + (ConfigObject.getInstance().getSearchFieldLocation() == SearchFieldLocation.TOP_SIDE ? 24 : 0) + 10), "") {
-                @Override
-                public void render(int mouseX, int mouseY, float delta) {
-                    clickable(ENTRY_LIST_WIDGET.getTotalPages() > 1);
-                    setText(String.format("%s/%s", ENTRY_LIST_WIDGET.getPage() + 1, Math.max(ENTRY_LIST_WIDGET.getTotalPages(), 1)));
-                    super.render(mouseX, mouseY, delta);
-                }
-                
-                @Override
-                public void onLabelClicked() {
-                    MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
-                    ENTRY_LIST_WIDGET.setPage(0);
-                    ENTRY_LIST_WIDGET.updateEntriesPosition();
-                }
-                
-                @Override
-                public boolean changeFocus(boolean boolean_1) {
-                    return false;
-                }
-            }.tooltip(() -> I18n.translate("text.rei.go_back_first_page")));
+            widgets.add(Widgets.createClickableLabel(new Point(bounds.x + (bounds.width / 2), bounds.y + (ConfigObject.getInstance().getSearchFieldLocation() == SearchFieldLocation.TOP_SIDE ? 24 : 0) + 10), "", label -> {
+                ENTRY_LIST_WIDGET.setPage(0);
+                ENTRY_LIST_WIDGET.updateEntriesPosition();
+            }).tooltipLine(I18n.translate("text.rei.go_back_first_page")).focusable(false).onRender(label -> {
+                label.setClickable(ENTRY_LIST_WIDGET.getTotalPages() > 1);
+                label.setText(String.format("%s/%s", ENTRY_LIST_WIDGET.getPage() + 1, Math.max(ENTRY_LIST_WIDGET.getTotalPages(), 1)));
+            }));
         }
         if (ConfigObject.getInstance().isCraftableFilterEnabled()) {
             this.widgets.add(craftableToggleButton = new CraftableToggleButtonWidget(getCraftableToggleArea()) {

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

@@ -29,9 +29,9 @@ import me.shedaniel.clothconfig2.gui.widget.DynamicNewSmoothScrollingEntryListWi
 import me.shedaniel.clothconfig2.impl.EasingMethod;
 import me.shedaniel.math.api.Point;
 import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.config.RecipeScreenType;
 import me.shedaniel.rei.gui.widget.ButtonWidget;
-import me.shedaniel.rei.gui.widget.LabelWidget;
 import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.gui.widget.WidgetWithBounds;
 import me.shedaniel.rei.impl.ScreenHelper;
@@ -117,9 +117,9 @@ public class PreRecipeViewingScreen extends Screen {
             }
         });
         this.widgets.add(new ScreenTypeSelection(width / 2 - 200 - 5, height / 2 - 112 / 2 - 10, 0));
-        this.widgets.add(LabelWidget.create(new Point(width / 2 - 200 - 5 + 104, height / 2 - 112 / 2 + 115), I18n.translate("config.roughlyenoughitems.recipeScreenType.original")).noShadow().color(-1124073473));
+        this.widgets.add(Widgets.createLabel(new Point(width / 2 - 200 - 5 + 104, height / 2 - 112 / 2 + 115), I18n.translate("config.roughlyenoughitems.recipeScreenType.original")).noShadow().color(-1124073473));
         this.widgets.add(new ScreenTypeSelection(width / 2 + 5, height / 2 - 112 / 2 - 10, 112));
-        this.widgets.add(LabelWidget.create(new Point(width / 2 + 5 + 104, height / 2 - 112 / 2 + 115), I18n.translate("config.roughlyenoughitems.recipeScreenType.villager")).noShadow().color(-1124073473));
+        this.widgets.add(Widgets.createLabel(new Point(width / 2 + 5 + 104, height / 2 - 112 / 2 + 115), I18n.translate("config.roughlyenoughitems.recipeScreenType.villager")).noShadow().color(-1124073473));
         this.children.addAll(widgets);
     }
     

+ 11 - 18
src/main/java/me/shedaniel/rei/gui/RecipeViewingScreen.java

@@ -31,6 +31,7 @@ import me.shedaniel.math.api.Point;
 import me.shedaniel.math.api.Rectangle;
 import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.api.*;
+import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.widget.*;
 import me.shedaniel.rei.impl.ClientHelperImpl;
 import me.shedaniel.rei.impl.ScreenHelper;
@@ -244,10 +245,9 @@ public class RecipeViewingScreen extends Screen implements RecipeScreen {
                 currentCategoryIndex = categories.size() - 1;
             ClientHelperImpl.getInstance().openRecipeViewingScreen(categoriesMap, categories.get(currentCategoryIndex).getIdentifier(), ingredientStackToNotice, resultStackToNotice);
         }).tooltip(() -> I18n.translate("text.rei.previous_category")));
-        widgets.add(LabelWidget.createClickable(new Point(bounds.getCenterX(), bounds.getY() + 7), selectedCategory.getCategoryName(), clickableLabelWidget -> {
-            MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
+        widgets.add(Widgets.createClickableLabel(new Point(bounds.getCenterX(), bounds.getY() + 7), selectedCategory.getCategoryName(), clickableLabelWidget -> {
             ClientHelper.getInstance().executeViewAllRecipesKeyBind();
-        }).tooltip(() -> I18n.translate("text.rei.view_all_categories")));
+        }).tooltipLine(I18n.translate("text.rei.view_all_categories")));
         widgets.add(categoryNext = ButtonWidget.create(new Rectangle(bounds.getMaxX() - 17, bounds.getY() + 5, 12, 12), new TranslatableText("text.rei.right_arrow"), buttonWidget -> {
             int currentCategoryIndex = categories.indexOf(selectedCategory);
             currentCategoryIndex++;
@@ -264,20 +264,13 @@ public class RecipeViewingScreen extends Screen implements RecipeScreen {
                 page = getTotalPages(selectedCategory) - 1;
             RecipeViewingScreen.this.init();
         }).tooltip(() -> I18n.translate("text.rei.previous_page")));
-        widgets.add(new ClickableLabelWidget(new Point(bounds.getCenterX(), bounds.getY() + 21), "") {
-            @Override
-            public void render(int mouseX, int mouseY, float delta) {
-                setText(String.format("%d/%d", page + 1, getTotalPages(selectedCategory)));
-                super.render(mouseX, mouseY, delta);
-            }
-            
-            @Override
-            public void onLabelClicked() {
-                MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
-                RecipeViewingScreen.this.choosePageActivated = true;
-                RecipeViewingScreen.this.init();
-            }
-        }.clickable(categoriesMap.get(selectedCategory).size() > getRecipesPerPageByHeight()).tooltip(() -> I18n.translate("text.rei.choose_page")));
+        widgets.add(Widgets.createClickableLabel(new Point(bounds.getCenterX(), bounds.getY() + 21), "", label -> {
+            RecipeViewingScreen.this.choosePageActivated = true;
+            RecipeViewingScreen.this.init();
+        }).onRender(label -> {
+            label.setText(String.format("%d/%d", page + 1, getTotalPages(selectedCategory)));
+            label.setClickable(categoriesMap.get(selectedCategory).size() > getRecipesPerPageByHeight());
+        }).tooltipSupplier(label -> label.isClickable() ? I18n.translate("text.rei.choose_page") : null));
         widgets.add(recipeNext = ButtonWidget.create(new Rectangle(bounds.getMaxX() - 17, bounds.getY() + 19, 12, 12), new TranslatableText("text.rei.right_arrow"), buttonWidget -> {
             page++;
             if (page >= getTotalPages(selectedCategory))
@@ -308,7 +301,7 @@ public class RecipeViewingScreen extends Screen implements RecipeScreen {
             final Supplier<RecipeDisplay> displaySupplier = () -> display;
             int displayWidth = selectedCategory.getDisplayWidth(displaySupplier.get());
             final Rectangle displayBounds = new Rectangle(getBounds().getCenterX() - displayWidth / 2, getBounds().y - 2 + 36 + recipeHeight * i + 4 * i, displayWidth, recipeHeight);
-            List<Widget> setupDisplay = selectedCategory.setupDisplay(displaySupplier, displayBounds);
+            List<Widget> setupDisplay = selectedCategory.setupDisplay(display, displayBounds);
             transformIngredientNotice(setupDisplay, ingredientStackToNotice);
             transformResultNotice(setupDisplay, resultStackToNotice);
             recipeBounds.put(displayBounds, setupDisplay);

+ 5 - 13
src/main/java/me/shedaniel/rei/gui/VillagerRecipeViewingScreen.java

@@ -31,6 +31,7 @@ import me.shedaniel.math.api.Point;
 import me.shedaniel.math.api.Rectangle;
 import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.api.*;
+import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.entries.RecipeEntry;
 import me.shedaniel.rei.gui.widget.*;
 import me.shedaniel.rei.impl.ClientHelperImpl;
@@ -173,7 +174,7 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
         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());
-        List<Widget> setupDisplay = category.setupDisplay(() -> display, recipeBounds);
+        List<Widget> setupDisplay = category.setupDisplay(display, recipeBounds);
         RecipeViewingScreen.transformIngredientNotice(setupDisplay, ingredientStackToNotice);
         RecipeViewingScreen.transformResultNotice(setupDisplay, resultStackToNotice);
         this.widgets.addAll(setupDisplay);
@@ -247,18 +248,9 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
         }));
         w.enabled = w2.enabled = categories.size() > tabsPerPage;
         
-        this.widgets.add(new ClickableLabelWidget(new Point(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"));
-            }
-        });
+        this.widgets.add(Widgets.createClickableLabel(new Point(bounds.x + 4 + scrollListBounds.width / 2, bounds.y + 6), categories.get(selectedCategoryIndex).getCategoryName(), label -> {
+            ClientHelper.getInstance().executeViewAllRecipesKeyBind();
+        }).tooltipLine(I18n.translate("text.rei.view_all_categories")).noShadow().color(0xFF404040, 0xFFBBBBBB).hoveredColor(0xFF0041FF, 0xFFFFBD4D));
         
         this.children.addAll(buttonWidgets);
         this.widgets.addAll(tabs);

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

@@ -23,10 +23,18 @@
 
 package me.shedaniel.rei.gui.widget;
 
-import me.shedaniel.math.api.Point;
+import me.shedaniel.math.Point;
 import me.shedaniel.rei.api.REIHelper;
+import me.shedaniel.rei.api.widgets.Widgets;
 import org.jetbrains.annotations.ApiStatus;
 
+import java.util.function.Consumer;
+
+/**
+ * @see Widgets#createClickableLabel(Point, String, Consumer)
+ */
+@Deprecated
+@ApiStatus.ScheduledForRemoval
 public abstract class ClickableLabelWidget extends LabelWidget {
     
     public boolean focused;
@@ -39,6 +47,12 @@ public abstract class ClickableLabelWidget extends LabelWidget {
         this.hoveredColor = REIHelper.getInstance().isDarkThemeEnabled() ? -1 : 0xFF66FFCC;
     }
     
+    @ApiStatus.Internal
+    protected ClickableLabelWidget(me.shedaniel.math.api.Point point, String text) {
+        super(point, text);
+        this.hoveredColor = REIHelper.getInstance().isDarkThemeEnabled() ? -1 : 0xFF66FFCC;
+    }
+    
     public LabelWidget hoveredColor(int hoveredColor) {
         this.hoveredColor = hoveredColor;
         return this;
@@ -58,7 +72,7 @@ public abstract class ClickableLabelWidget extends LabelWidget {
         int color = getDefaultColor();
         if (isClickable() && isHovered(mouseX, mouseY))
             color = getHoveredColor();
-        Point pos = getPosition();
+        Point pos = getLocation();
         int width = font.getStringWidth(getText());
         if (isCentered()) {
             if (isHasShadows())

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

@@ -33,6 +33,7 @@ import me.shedaniel.math.api.Rectangle;
 import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.RoughlyEnoughItemsCore;
 import me.shedaniel.rei.api.*;
+import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.ContainerScreenOverlay;
 import me.shedaniel.rei.gui.config.ItemListOrdering;
 import me.shedaniel.rei.impl.ScreenHelper;
@@ -593,7 +594,7 @@ public class EntryListWidget extends WidgetWithBounds {
                 int skippedFavorites = page * (entries.size() - width);
                 int j = 0;
                 if (skippedFavorites < favorites.size()) {
-                    renders.add(LabelWidget.create(new Point(innerBounds.x + 2, innerBounds.y + 6), I18n.translate("text.rei.favorites")).leftAligned());
+                    renders.add(Widgets.createLabel(new Point(innerBounds.x + 2, innerBounds.y + 6), I18n.translate("text.rei.favorites")).leftAligned());
                     j += width;
                 }
                 List<EntryStack> subFavoritesList = favorites.stream().skip(skippedFavorites).limit(Math.max(0, entries.size() - width)).collect(Collectors.toList());

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

@@ -29,6 +29,7 @@ import me.shedaniel.math.api.Point;
 import me.shedaniel.math.api.Rectangle;
 import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.api.*;
+import me.shedaniel.rei.api.widgets.Slot;
 import me.shedaniel.rei.gui.ContainerScreenOverlay;
 import me.shedaniel.rei.impl.ScreenHelper;
 import me.shedaniel.rei.utils.CollectionUtils;
@@ -42,7 +43,7 @@ import org.jetbrains.annotations.ApiStatus;
 
 import java.util.*;
 
-public class EntryWidget extends WidgetWithBounds {
+public class EntryWidget extends Slot {
     
     protected static final Identifier RECIPE_GUI = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
     protected static final Identifier RECIPE_GUI_DARK = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer_dark.png");
@@ -66,14 +67,25 @@ public class EntryWidget extends WidgetWithBounds {
         this.entryStacks = new ArrayList<>();
     }
     
+    /**
+     * @see me.shedaniel.rei.api.widgets.Widgets#createSlot(me.shedaniel.math.Point)
+     */
+    @ApiStatus.ScheduledForRemoval
+    @Deprecated
     public static EntryWidget create(int x, int y) {
         return create(new Point(x, y));
     }
     
+    /**
+     * @see me.shedaniel.rei.api.widgets.Widgets#createSlot(me.shedaniel.math.Point)
+     */
+    @ApiStatus.ScheduledForRemoval
+    @Deprecated
     public static EntryWidget create(Point point) {
         return new EntryWidget(point);
     }
     
+    @Override
     public EntryWidget unmarkInputOrOutput() {
         noticeMark = 0;
         return this;
@@ -89,15 +101,41 @@ public class EntryWidget extends WidgetWithBounds {
         return this;
     }
     
-    @ApiStatus.Internal
+    @Override
     public byte getNoticeMark() {
         return noticeMark;
     }
     
+    @Override
+    public void setNoticeMark(byte noticeMark) {
+        this.noticeMark = noticeMark;
+    }
+    
+    @Override
+    public void setInteractable(boolean interactable) {
+        interactable(interactable);
+    }
+    
+    @Override
+    public boolean isInteractable() {
+        return this.interactable;
+    }
+    
+    @Override
+    public void setInteractableFavorites(boolean interactableFavorites) {
+        interactableFavorites(interactableFavorites);
+    }
+    
+    @Override
+    public boolean isInteractableFavorites() {
+        return interactableFavorites;
+    }
+    
     public EntryWidget disableInteractions() {
         return interactable(false);
     }
     
+    @Override
     public EntryWidget interactable(boolean b) {
         interactable = b;
         interactableFavorites = interactableFavorites && interactable;
@@ -108,6 +146,7 @@ public class EntryWidget extends WidgetWithBounds {
         return interactableFavorites(false);
     }
     
+    @Override
     public EntryWidget interactableFavorites(boolean b) {
         interactableFavorites = b && interactable;
         return this;
@@ -122,6 +161,16 @@ public class EntryWidget extends WidgetWithBounds {
         return this;
     }
     
+    @Override
+    public boolean isHighlightEnabled() {
+        return highlight;
+    }
+    
+    @Override
+    public void setHighlightEnabled(boolean highlights) {
+        highlight(highlights);
+    }
+    
     public EntryWidget noTooltips() {
         return tooltips(false);
     }
@@ -131,6 +180,16 @@ public class EntryWidget extends WidgetWithBounds {
         return this;
     }
     
+    @Override
+    public void setTooltipsEnabled(boolean tooltipsEnabled) {
+        tooltips(tooltipsEnabled);
+    }
+    
+    @Override
+    public boolean isTooltipsEnabled() {
+        return tooltips;
+    }
+    
     public EntryWidget noBackground() {
         return background(false);
     }
@@ -140,16 +199,33 @@ public class EntryWidget extends WidgetWithBounds {
         return this;
     }
     
+    @Override
+    public void setBackgroundEnabled(boolean backgroundEnabled) {
+        background(backgroundEnabled);
+    }
+    
+    @Override
+    public boolean isBackgroundEnabled() {
+        return background;
+    }
+    
     public EntryWidget clearStacks() {
         entryStacks.clear();
         return this;
     }
     
+    @Override
+    public Slot clearEntries() {
+        return clearStacks();
+    }
+    
+    @Override
     public EntryWidget entry(EntryStack stack) {
         entryStacks.add(stack);
         return this;
     }
     
+    @Override
     public EntryWidget entries(Collection<EntryStack> stacks) {
         entryStacks.addAll(stacks);
         return this;
@@ -163,6 +239,11 @@ public class EntryWidget extends WidgetWithBounds {
         return entryStacks.get(MathHelper.floor((System.currentTimeMillis() / 500 % (double) entryStacks.size()) / 1f));
     }
     
+    @Override
+    public List<EntryStack> getEntries() {
+        return entryStacks;
+    }
+    
     public List<EntryStack> entries() {
         return entryStacks;
     }
@@ -225,6 +306,7 @@ public class EntryWidget extends WidgetWithBounds {
         }
     }
     
+    @Override
     public QueuedTooltip getCurrentTooltip(int mouseX, int mouseY) {
         return getCurrentEntry().getTooltip(mouseX, mouseY);
     }

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

@@ -23,9 +23,10 @@
 
 package me.shedaniel.rei.gui.widget;
 
-import me.shedaniel.math.api.Point;
+import me.shedaniel.math.Point;
 import me.shedaniel.math.api.Rectangle;
 import me.shedaniel.rei.api.REIHelper;
+import me.shedaniel.rei.api.widgets.Widgets;
 import net.minecraft.client.gui.Element;
 import org.jetbrains.annotations.ApiStatus;
 
@@ -35,6 +36,12 @@ import java.util.Optional;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
 
+/**
+ * @see Widgets#createLabel(Point, String)
+ * @see Widgets#createClickableLabel(Point, String, Consumer)
+ */
+@Deprecated
+@ApiStatus.ScheduledForRemoval
 public class LabelWidget extends WidgetWithBounds {
     
     private Point pos;
@@ -44,6 +51,11 @@ public class LabelWidget extends WidgetWithBounds {
     private boolean centered = true;
     private Supplier<String> tooltipSupplier;
     
+    @ApiStatus.Internal
+    public LabelWidget(me.shedaniel.math.api.Point point, String text) {
+        this((Point) point, text);
+    }
+    
     @ApiStatus.Internal
     public LabelWidget(Point point, String text) {
         this.pos = point;
@@ -55,6 +67,14 @@ public class LabelWidget extends WidgetWithBounds {
         return new LabelWidget(point, text);
     }
     
+    public static LabelWidget create(me.shedaniel.math.api.Point point, String text) {
+        return new LabelWidget(point, text);
+    }
+    
+    public static ClickableLabelWidget createClickable(me.shedaniel.math.api.Point point, String text, Consumer<ClickableLabelWidget> onClicked) {
+        return createClickable((Point) point, text, onClicked);
+    }
+    
     public static ClickableLabelWidget createClickable(Point point, String text, Consumer<ClickableLabelWidget> onClicked) {
         ClickableLabelWidget[] widget = {null};
         widget[0] = new ClickableLabelWidget(point, text) {
@@ -110,11 +130,25 @@ public class LabelWidget extends WidgetWithBounds {
         this.defaultColor = defaultColor;
     }
     
-    public Point getPosition() {
+    /**
+     * @return the position of this label
+     * @deprecated Use {@link #getLocation()}
+     */
+    @Deprecated
+    @ApiStatus.ScheduledForRemoval
+    public me.shedaniel.math.api.Point getPosition() {
+        return new me.shedaniel.math.api.Point(getLocation());
+    }
+    
+    public Point getLocation() {
         return pos;
     }
     
-    public LabelWidget setPosition(Point position) {
+    public LabelWidget setPosition(me.shedaniel.math.api.Point position) {
+        return setLocation(position);
+    }
+    
+    public LabelWidget setLocation(Point position) {
         this.pos = position;
         return this;
     }

+ 19 - 5
src/main/java/me/shedaniel/rei/gui/widget/QueuedTooltip.java

@@ -25,7 +25,7 @@ package me.shedaniel.rei.gui.widget;
 
 
 import com.google.common.collect.Lists;
-import me.shedaniel.math.api.Point;
+import me.shedaniel.math.Point;
 import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.api.REIHelper;
 import org.jetbrains.annotations.ApiStatus;
@@ -44,6 +44,14 @@ public class QueuedTooltip {
         this.text = Lists.newArrayList(text);
     }
     
+    public static QueuedTooltip create(me.shedaniel.math.api.Point location, List<String> text) {
+        return new QueuedTooltip(location, text);
+    }
+    
+    public static QueuedTooltip create(me.shedaniel.math.api.Point location, String... text) {
+        return QueuedTooltip.create(location, Lists.newArrayList(text));
+    }
+    
     public static QueuedTooltip create(Point location, List<String> text) {
         return new QueuedTooltip(location, text);
     }
@@ -71,16 +79,22 @@ public class QueuedTooltip {
         return consumer;
     }
     
-    public Point getLocation() {
-        return location;
+    /**
+     * @return the absolute location of the tooltip.
+     * @deprecated Use {@link #getX()} and {@link #getY()} instead.
+     */
+    @Deprecated
+    @ApiStatus.ScheduledForRemoval
+    public me.shedaniel.math.api.Point getLocation() {
+        return new me.shedaniel.math.api.Point(location);
     }
     
     public int getX() {
-        return getLocation().x;
+        return location.x;
     }
     
     public int getY() {
-        return getLocation().y;
+        return location.y;
     }
     
     public List<String> getText() {

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

@@ -34,6 +34,11 @@ import org.jetbrains.annotations.ApiStatus;
 import java.util.Collections;
 import java.util.List;
 
+/**
+ * @see me.shedaniel.rei.api.widgets.Widgets#createArrow(me.shedaniel.math.Point)
+ */
+@ApiStatus.ScheduledForRemoval
+@Deprecated
 public class RecipeArrowWidget extends WidgetWithBounds {
     
     private int x, y;

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

@@ -25,9 +25,19 @@ package me.shedaniel.rei.gui.widget;
 
 import me.shedaniel.math.api.Rectangle;
 import me.shedaniel.rei.api.ConfigObject;
+import org.jetbrains.annotations.ApiStatus;
 
 public class RecipeBaseWidget extends PanelWidget {
     
+    /**
+     * Creates a recipe base drawable
+     *
+     * @param bounds the bounds of the base
+     * @see me.shedaniel.rei.api.widgets.Widgets#createRecipeBase(me.shedaniel.math.Rectangle)
+     * @see me.shedaniel.rei.api.widgets.Widgets#createRecipeBase(me.shedaniel.math.Rectangle, int)
+     */
+    @ApiStatus.Internal
+    @Deprecated
     public RecipeBaseWidget(Rectangle bounds) {
         super(bounds);
     }

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

@@ -25,9 +25,19 @@ package me.shedaniel.rei.gui.widget;
 
 import me.shedaniel.math.api.Rectangle;
 import me.shedaniel.rei.api.REIHelper;
+import org.jetbrains.annotations.ApiStatus;
 
 public class SlotBaseWidget extends RecipeBaseWidget {
     
+    /**
+     * Creates a recipe base drawable
+     *
+     * @param bounds the bounds of the base
+     * @see me.shedaniel.rei.api.widgets.Widgets#createSlotBase(me.shedaniel.math.Rectangle)
+     * @see me.shedaniel.rei.api.widgets.Widgets#createSlotBase(me.shedaniel.math.Rectangle, int)
+     */
+    @ApiStatus.Internal
+    @Deprecated
     public SlotBaseWidget(Rectangle bounds) {
         super(bounds);
     }

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

@@ -57,6 +57,7 @@ public abstract class Widget extends AbstractParentElement implements Drawable {
         return false;
     }
     
+    @SuppressWarnings("RedundantCast")
     public final boolean containsMouse(int mouseX, int mouseY) {
         return containsMouse((double) mouseX, (double) mouseY);
     }

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

@@ -371,9 +371,9 @@ public class ConfigObjectImpl implements ConfigObject, ConfigData {
     
     public static class Appearance {
         @UseSpecialRecipeTypeScreen private RecipeScreenType recipeScreenType = RecipeScreenType.UNSET;
+        @Comment("Declares the appearance of REI windows.") private boolean darkTheme = false;
         @Comment("The ordering of the items on the item panel.") @UseEnumSelectorInstead
         private ItemListOrderingConfig itemListOrdering = ItemListOrderingConfig.REGISTRY_ASCENDING;
-        @Comment("Declares the appearance of REI windows.") private boolean darkTheme = false;
         @Comment("Declares the position of the search field.") @UseEnumSelectorInstead
         private SearchFieldLocation searchFieldLocation = SearchFieldLocation.CENTER;
         @Comment("Declares the position of the item list panel.") private boolean mirrorItemPanel = false;

+ 78 - 0
src/main/java/me/shedaniel/rei/impl/widgets/ArrowWidget.java

@@ -0,0 +1,78 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl.widgets;
+
+import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.rei.api.widgets.Arrow;
+import me.shedaniel.rei.plugin.DefaultPlugin;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.util.math.MathHelper;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+public class ArrowWidget extends Arrow {
+    @NotNull
+    private Rectangle bounds;
+    private double animationDuration = -1;
+    
+    public ArrowWidget(@NotNull me.shedaniel.math.Rectangle bounds) {
+        this.bounds = new Rectangle(Objects.requireNonNull(bounds));
+    }
+    
+    @Override
+    public double getAnimationDuration() {
+        return animationDuration;
+    }
+    
+    @Override
+    public void setAnimationDuration(double animationDurationMS) {
+        this.animationDuration = animationDurationMS;
+        if (this.animationDuration <= 0)
+            this.animationDuration = -1;
+    }
+    
+    @Override
+    public @NotNull Rectangle getBounds() {
+        return bounds;
+    }
+    
+    @Override
+    public void render(int mouseX, int mouseY, float delta) {
+        MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
+        blit(getX(), getY(), 106, 91, 24, 17);
+        if (getAnimationDuration() > 0) {
+            int width = MathHelper.ceil((System.currentTimeMillis() / (animationDuration / 24) % 24d) / 1f);
+            blit(getX(), getY(), 82, 91, width, 17);
+        }
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        return Collections.emptyList();
+    }
+}

+ 78 - 0
src/main/java/me/shedaniel/rei/impl/widgets/BurningFireWidget.java

@@ -0,0 +1,78 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl.widgets;
+
+import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.rei.api.widgets.BurningFire;
+import me.shedaniel.rei.plugin.DefaultPlugin;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.util.math.MathHelper;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+public class BurningFireWidget extends BurningFire {
+    @NotNull
+    private Rectangle bounds;
+    private double animationDuration = -1;
+    
+    public BurningFireWidget(@NotNull me.shedaniel.math.Rectangle bounds) {
+        this.bounds = new Rectangle(Objects.requireNonNull(bounds));
+    }
+    
+    @Override
+    public double getAnimationDuration() {
+        return animationDuration;
+    }
+    
+    @Override
+    public void setAnimationDuration(double animationDurationMS) {
+        this.animationDuration = animationDurationMS;
+        if (this.animationDuration <= 0)
+            this.animationDuration = -1;
+    }
+    
+    @Override
+    public @NotNull Rectangle getBounds() {
+        return bounds;
+    }
+    
+    @Override
+    public void render(int mouseX, int mouseY, float delta) {
+        MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
+        blit(getX(), getY(), 1, 74, 14, 14);
+        if (getAnimationDuration() > 0) {
+            int height = 14 - MathHelper.ceil((System.currentTimeMillis() / (animationDuration / 14) % 14d) / 1f);
+            blit(getX(), getY() + 14 - height, 82, 77 + (14 - height), 14, height);
+        }
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        return Collections.emptyList();
+    }
+}

+ 49 - 0
src/main/java/me/shedaniel/rei/impl/widgets/DrawableWidget.java

@@ -0,0 +1,49 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl.widgets;
+
+import me.shedaniel.rei.api.DrawableConsumer;
+import me.shedaniel.rei.gui.widget.Widget;
+import net.minecraft.client.gui.Element;
+
+import java.util.Collections;
+import java.util.List;
+
+public final class DrawableWidget extends Widget {
+    private DrawableConsumer drawable;
+    
+    public DrawableWidget(DrawableConsumer drawable) {
+        this.drawable = drawable;
+    }
+    
+    @Override
+    public void render(int mouseX, int mouseY, float delta) {
+        this.drawable.render(this, mouseX, mouseY, delta);
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        return Collections.emptyList();
+    }
+}

+ 67 - 0
src/main/java/me/shedaniel/rei/impl/widgets/FillRectangleDrawableConsumer.java

@@ -0,0 +1,67 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl.widgets;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import me.shedaniel.math.Rectangle;
+import me.shedaniel.rei.api.DrawableConsumer;
+import net.minecraft.client.gui.DrawableHelper;
+import net.minecraft.client.render.BufferBuilder;
+import net.minecraft.client.render.Tessellator;
+import net.minecraft.client.render.VertexFormats;
+
+public class FillRectangleDrawableConsumer implements DrawableConsumer {
+    private Rectangle rectangle;
+    private int color;
+    
+    public FillRectangleDrawableConsumer(Rectangle rectangle, int color) {
+        this.rectangle = rectangle;
+        this.color = color;
+    }
+    
+    @Override
+    public void render(DrawableHelper helper, int mouseX, int mouseY, float delta) {
+        float a = (color >> 24 & 255) / 255.0F;
+        float r = (color >> 16 & 255) / 255.0F;
+        float g = (color >> 8 & 255) / 255.0F;
+        float b = (color & 255) / 255.0F;
+        RenderSystem.disableTexture();
+        RenderSystem.enableBlend();
+        RenderSystem.disableAlphaTest();
+        RenderSystem.defaultBlendFunc();
+        RenderSystem.shadeModel(7425);
+        Tessellator tessellator = Tessellator.getInstance();
+        BufferBuilder bufferBuilder = tessellator.getBuffer();
+        bufferBuilder.begin(7, VertexFormats.POSITION_COLOR);
+        bufferBuilder.vertex(rectangle.getMaxX(), rectangle.getMinY(), helper.getBlitOffset()).color(r, g, b, a).next();
+        bufferBuilder.vertex(rectangle.getMinX(), rectangle.getMinY(), helper.getBlitOffset()).color(r, g, b, a).next();
+        bufferBuilder.vertex(rectangle.getMinX(), rectangle.getMaxY(), helper.getBlitOffset()).color(r, g, b, a).next();
+        bufferBuilder.vertex(rectangle.getMaxX(), rectangle.getMaxY(), helper.getBlitOffset()).color(r, g, b, a).next();
+        tessellator.draw();
+        RenderSystem.shadeModel(7424);
+        RenderSystem.disableBlend();
+        RenderSystem.enableAlphaTest();
+        RenderSystem.enableTexture();
+    }
+}

+ 268 - 0
src/main/java/me/shedaniel/rei/impl/widgets/LabelWidget.java

@@ -0,0 +1,268 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl.widgets;
+
+import me.shedaniel.math.Point;
+import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.rei.api.REIHelper;
+import me.shedaniel.rei.api.widgets.Label;
+import me.shedaniel.rei.api.widgets.Widgets;
+import me.shedaniel.rei.gui.widget.QueuedTooltip;
+import net.minecraft.client.gui.Element;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+public final class LabelWidget extends Label {
+    
+    private boolean focused = false;
+    private boolean clickable = false;
+    private int horizontalAlignment = Label.CENTER;
+    private boolean hasShadow = true;
+    private boolean focusable = true;
+    private int color = REIHelper.getInstance().isDarkThemeEnabled() ? 0xFFBBBBBB : -1;
+    private int hoveredColor = REIHelper.getInstance().isDarkThemeEnabled() ? -1 : 0xFF66FFCC;
+    private Point point;
+    @Nullable private Function<Label, @Nullable String> tooltip;
+    @Nullable private Consumer<Label> onClick;
+    @Nullable private Consumer<Label> onRender;
+    @NotNull private String text;
+    
+    public LabelWidget(Point point, @NotNull String text) {
+        this.point = point;
+        Objects.requireNonNull(this.text = text);
+    }
+    
+    @Override
+    public boolean isClickable() {
+        return clickable;
+    }
+    
+    @Override
+    public void setClickable(boolean clickable) {
+        this.clickable = clickable;
+    }
+    
+    @Nullable
+    @Override
+    public Consumer<Label> getOnClick() {
+        return onClick;
+    }
+    
+    @Override
+    public void setOnClick(@Nullable Consumer<Label> onClick) {
+        this.onClick = onClick;
+    }
+    
+    @Nullable
+    @Override
+    public Consumer<Label> getOnRender() {
+        return onRender;
+    }
+    
+    @Override
+    public void setOnRender(@Nullable Consumer<Label> onRender) {
+        this.onRender = onRender;
+    }
+    
+    @Override
+    public boolean isFocusable() {
+        return focusable;
+    }
+    
+    @Override
+    public void setFocusable(boolean focusable) {
+        this.focusable = focusable;
+    }
+    
+    @Override
+    @Nullable
+    public String getTooltip() {
+        if (tooltip == null)
+            return null;
+        return tooltip.apply(this);
+    }
+    
+    @Override
+    public void setTooltip(@Nullable Function<Label, @Nullable String> tooltip) {
+        this.tooltip = tooltip;
+    }
+    
+    @Override
+    public int getHorizontalAlignment() {
+        return horizontalAlignment;
+    }
+    
+    @Override
+    public void setHorizontalAlignment(int horizontalAlignment) {
+        this.horizontalAlignment = horizontalAlignment;
+    }
+    
+    @Override
+    public boolean hasShadow() {
+        return hasShadow;
+    }
+    
+    @Override
+    public void setShadow(boolean hasShadow) {
+        this.hasShadow = hasShadow;
+    }
+    
+    @Override
+    public int getColor() {
+        return color;
+    }
+    
+    @Override
+    public void setColor(int color) {
+        this.color = color;
+    }
+    
+    @Override
+    public int getHoveredColor() {
+        return hoveredColor;
+    }
+    
+    @Override
+    public void setHoveredColor(int hoveredColor) {
+        this.hoveredColor = hoveredColor;
+    }
+    
+    @Override
+    public @NotNull Point getPoint() {
+        return point;
+    }
+    
+    @Override
+    public void setPoint(@NotNull Point point) {
+        this.point = Objects.requireNonNull(point);
+    }
+    
+    @Override
+    public @NotNull String getText() {
+        return text;
+    }
+    
+    @Override
+    public void setText(@NotNull String text) {
+        this.text = Objects.requireNonNull(text);
+    }
+    
+    @Override
+    public Rectangle getBounds() {
+        int width = font.getStringWidth(text);
+        Point point = getPoint();
+        if (getHorizontalAlignment() == LEFT_ALIGNED)
+            return new Rectangle(point.x - 1, point.y - 5, width + 2, 14);
+        if (getHorizontalAlignment() == RIGHT_ALIGNED)
+            return new Rectangle(point.x - width - 1, point.y - 5, width + 2, 14);
+        return new Rectangle(point.x - width / 2 - 1, point.y - 5, width + 2, 14);
+    }
+    
+    @Override
+    public void render(int mouseX, int mouseY, float delta) {
+        if (getOnRender() != null)
+            getOnRender().accept(this);
+        int color = getColor();
+        if (isClickable() && isHovered(mouseX, mouseY))
+            color = getHoveredColor();
+        Point pos = getPoint();
+        int width = font.getStringWidth(getText());
+        switch (getHorizontalAlignment()) {
+            case LEFT_ALIGNED:
+                if (hasShadow())
+                    font.drawWithShadow(getText(), pos.x, pos.y, color);
+                else
+                    font.draw(getText(), pos.x, pos.y, color);
+                break;
+            case RIGHT_ALIGNED:
+                if (hasShadow())
+                    font.drawWithShadow(getText(), pos.x - width, pos.y, color);
+                else
+                    font.draw(getText(), pos.x - width, pos.y, color);
+                break;
+            case CENTER:
+            default:
+                if (hasShadow())
+                    font.drawWithShadow(getText(), pos.x - width / 2f, pos.y, color);
+                else
+                    font.draw(getText(), pos.x - width / 2f, pos.y, color);
+                break;
+        }
+        if (isHovered(mouseX, mouseY)) {
+            String tooltip = getTooltip();
+            if (tooltip != null) {
+                if (!focused && containsMouse(mouseX, mouseY))
+                    QueuedTooltip.create(tooltip.split("\n")).queue();
+                else if (focused)
+                    QueuedTooltip.create(point, tooltip.split("\n")).queue();
+            }
+        }
+    }
+    
+    @Override
+    public boolean mouseClicked(double mouseX, double mouseY, int button) {
+        if (button == 0 && isClickable() && containsMouse(mouseX, mouseY)) {
+            Widgets.produceClickSound();
+            if (onClick != null)
+                onClick.accept(this);
+            return true;
+        }
+        return false;
+    }
+    
+    @Override
+    public boolean keyPressed(int int_1, int int_2, int int_3) {
+        if (!isClickable() || !isFocusable() || !focused)
+            return false;
+        if (int_1 != 257 && int_1 != 32 && int_1 != 335)
+            return false;
+        Widgets.produceClickSound();
+        if (onClick != null)
+            onClick.accept(this);
+        return true;
+    }
+    
+    @Override
+    public boolean changeFocus(boolean boolean_1) {
+        if (!isClickable() || !isFocusable())
+            return false;
+        this.focused = !this.focused;
+        return true;
+    }
+    
+    public boolean isHovered(int mouseX, int mouseY) {
+        return containsMouse(mouseX, mouseY) || focused;
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        return Collections.emptyList();
+    }
+}

+ 162 - 0
src/main/java/me/shedaniel/rei/impl/widgets/PanelWidget.java

@@ -0,0 +1,162 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl.widgets;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import me.shedaniel.math.Rectangle;
+import me.shedaniel.rei.api.ConfigObject;
+import me.shedaniel.rei.api.REIHelper;
+import me.shedaniel.rei.api.widgets.Panel;
+import me.shedaniel.rei.gui.config.RecipeBorderType;
+import me.shedaniel.rei.gui.config.RecipeScreenType;
+import net.minecraft.client.gui.Element;
+import net.minecraft.util.Identifier;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Predicate;
+
+public final class PanelWidget extends Panel {
+    private static final Identifier CHEST_GUI_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
+    private static final Identifier CHEST_GUI_TEXTURE_DARK = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer_dark.png");
+    
+    private static final PanelWidget TEMP = new PanelWidget(new Rectangle());
+    private Rectangle bounds;
+    private int color = -1;
+    private int innerColor = REIHelper.getInstance().isDarkThemeEnabled() ? -13750738 : -3750202;
+    private int xTextureOffset = 0;
+    private int yTextureOffset = RecipeBorderType.DEFAULT.getYOffset();
+    @NotNull
+    private Predicate<Panel> rendering = PanelWidget::isRendering;
+    
+    public static boolean isRendering(Panel panel) {
+        return ConfigObject.getInstance().getRecipeScreenType() != RecipeScreenType.VILLAGER;
+    }
+    
+    public PanelWidget(Rectangle bounds) {
+        this.bounds = bounds;
+    }
+    
+    public static void render(Rectangle bounds, int color) {
+        TEMP.bounds = bounds;
+        TEMP.color = color;
+        TEMP.render(0, 0, 0);
+    }
+    
+    @Override
+    public int getInnerColor() {
+        return innerColor;
+    }
+    
+    @Override
+    public void setInnerColor(int innerColor) {
+        this.innerColor = innerColor;
+    }
+    
+    @Override
+    public int getXTextureOffset() {
+        return xTextureOffset;
+    }
+    
+    @Override
+    public void setXTextureOffset(int xTextureOffset) {
+        this.xTextureOffset = xTextureOffset;
+    }
+    
+    @Override
+    public int getYTextureOffset() {
+        return yTextureOffset;
+    }
+    
+    @Override
+    public void setYTextureOffset(int yTextureOffset) {
+        this.yTextureOffset = yTextureOffset;
+    }
+    
+    @Override
+    public int getColor() {
+        return color;
+    }
+    
+    @Override
+    public void setColor(int color) {
+        this.color = color;
+    }
+    
+    @Override
+    public @NotNull Predicate<Panel> getRendering() {
+        return rendering;
+    }
+    
+    @Override
+    public void setRendering(@NotNull Predicate<Panel> rendering) {
+        this.rendering = Objects.requireNonNull(rendering);
+    }
+    
+    @Override
+    public me.shedaniel.math.api.Rectangle getBounds() {
+        return new me.shedaniel.math.api.Rectangle(bounds);
+    }
+    
+    @Override
+    public void render(int mouseX, int mouseY, float delta) {
+        if (!getRendering().test(this))
+            return;
+        float alpha = ((color >> 24) & 0xFF) / 255f;
+        float red = ((color >> 16) & 0xFF) / 255f;
+        float green = ((color >> 8) & 0xFF) / 255f;
+        float blue = (color & 0xFF) / 255f;
+        RenderSystem.color4f(red, green, blue, alpha);
+        minecraft.getTextureManager().bindTexture(REIHelper.getInstance().isDarkThemeEnabled() ? CHEST_GUI_TEXTURE_DARK : CHEST_GUI_TEXTURE);
+        int x = bounds.x, y = bounds.y, width = bounds.width, height = bounds.height;
+        int xTextureOffset = getXTextureOffset();
+        int yTextureOffset = getYTextureOffset();
+        
+        //Four Corners
+        this.blit(x, y, 106 + xTextureOffset, 124 + yTextureOffset, 4, 4);
+        this.blit(x + width - 4, y, 252 + xTextureOffset, 124 + yTextureOffset, 4, 4);
+        this.blit(x, y + height - 4, 106 + xTextureOffset, 186 + yTextureOffset, 4, 4);
+        this.blit(x + width - 4, y + height - 4, 252 + xTextureOffset, 186 + yTextureOffset, 4, 4);
+        
+        //Sides
+        for (int xx = 4; xx < width - 4; xx += 128) {
+            int thisWidth = Math.min(128, width - 4 - xx);
+            this.blit(x + xx, y, 110 + xTextureOffset, 124 + yTextureOffset, thisWidth, 4);
+            this.blit(x + xx, y + height - 4, 110 + xTextureOffset, 186 + yTextureOffset, thisWidth, 4);
+        }
+        for (int yy = 4; yy < height - 4; yy += 50) {
+            int thisHeight = Math.min(50, height - 4 - yy);
+            this.blit(x, y + yy, 106 + xTextureOffset, 128 + yTextureOffset, 4, thisHeight);
+            this.blit(x + width - 4, y + yy, 252 + xTextureOffset, 128 + yTextureOffset, 4, thisHeight);
+        }
+        fillGradient(x + 4, y + 4, x + width - 4, y + height - 4, getInnerColor(), getInnerColor());
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        return Collections.emptyList();
+    }
+}

+ 75 - 0
src/main/java/me/shedaniel/rei/impl/widgets/TexturedDrawableConsumer.java

@@ -0,0 +1,75 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl.widgets;
+
+import me.shedaniel.rei.api.DrawableConsumer;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawableHelper;
+import net.minecraft.client.render.BufferBuilder;
+import net.minecraft.client.render.Tessellator;
+import net.minecraft.client.render.VertexFormats;
+import net.minecraft.util.Identifier;
+import org.lwjgl.opengl.GL11;
+
+public class TexturedDrawableConsumer implements DrawableConsumer {
+    
+    private Identifier identifier;
+    private int x, y, width, height, uWidth, vHeight, textureWidth, textureHeight;
+    private float u, v;
+    
+    public TexturedDrawableConsumer(Identifier identifier, int x, int y, int width, int height, float u, float v, int uWidth, int vHeight, int textureWidth, int textureHeight) {
+        this.identifier = identifier;
+        this.x = x;
+        this.y = y;
+        this.width = width;
+        this.height = height;
+        this.u = u;
+        this.v = v;
+        this.uWidth = uWidth;
+        this.vHeight = vHeight;
+        this.textureWidth = textureWidth;
+        this.textureHeight = textureHeight;
+    }
+    
+    @Override
+    public void render(DrawableHelper helper, int mouseX, int mouseY, float delta) {
+        MinecraftClient.getInstance().getTextureManager().bindTexture(identifier);
+        innerBlit(x, x + width, y, y + height, helper.getBlitOffset(), uWidth, vHeight, u, v, textureWidth, textureHeight);
+    }
+    
+    private static void innerBlit(int xStart, int xEnd, int yStart, int yEnd, int z, int width, int height, float u, float v, int texWidth, int texHeight) {
+        innerBlit(xStart, xEnd, yStart, yEnd, z, u / texWidth, (u + width) / texWidth, v / texHeight, (v + height) / texHeight);
+    }
+    
+    protected static void innerBlit(int xStart, int xEnd, int yStart, int yEnd, int z, float uStart, float uEnd, float vStart, float vEnd) {
+        Tessellator tessellator = Tessellator.getInstance();
+        BufferBuilder bufferBuilder = tessellator.getBuffer();
+        bufferBuilder.begin(GL11.GL_QUADS, VertexFormats.POSITION_TEXTURE);
+        bufferBuilder.vertex(xStart, yEnd, z).texture(uStart, vEnd).next();
+        bufferBuilder.vertex(xEnd, yEnd, z).texture(uEnd, vEnd).next();
+        bufferBuilder.vertex(xEnd, yStart, z).texture(uEnd, vStart).next();
+        bufferBuilder.vertex(xStart, yStart, z).texture(uStart, vStart).next();
+        tessellator.draw();
+    }
+}

+ 27 - 0
src/main/java/me/shedaniel/rei/impl/widgets/package-info.java

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

+ 14 - 11
src/main/java/me/shedaniel/rei/plugin/beacon/DefaultBeaconBaseCategory.java

@@ -28,12 +28,17 @@ import com.mojang.blaze3d.systems.RenderSystem;
 import me.shedaniel.clothconfig2.ClothConfigInitializer;
 import me.shedaniel.clothconfig2.api.ScissorsHandler;
 import me.shedaniel.clothconfig2.gui.widget.DynamicEntryListWidget;
-import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.math.Point;
+import me.shedaniel.math.Rectangle;
 import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.RecipeCategory;
+import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.entries.RecipeEntry;
-import me.shedaniel.rei.gui.widget.*;
+import me.shedaniel.rei.gui.widget.EntryWidget;
+import me.shedaniel.rei.gui.widget.QueuedTooltip;
+import me.shedaniel.rei.gui.widget.Widget;
+import me.shedaniel.rei.gui.widget.WidgetWithBounds;
 import me.shedaniel.rei.plugin.DefaultPlugin;
 import me.shedaniel.rei.utils.CollectionUtils;
 import net.minecraft.block.Blocks;
@@ -48,7 +53,6 @@ import net.minecraft.util.math.MathHelper;
 import org.jetbrains.annotations.Nullable;
 
 import java.util.List;
-import java.util.function.Supplier;
 
 public class DefaultBeaconBaseCategory implements RecipeCategory<DefaultBeaconBaseDisplay> {
     @Override
@@ -82,19 +86,18 @@ public class DefaultBeaconBaseCategory implements RecipeCategory<DefaultBeaconBa
             }
             
             @Override
-            public void render(Rectangle rectangle, int mouseX, int mouseY, float delta) {
+            public void render(me.shedaniel.math.api.Rectangle rectangle, int mouseX, int mouseY, float delta) {
                 MinecraftClient.getInstance().textRenderer.draw(name, rectangle.x + 5, rectangle.y + 6, -1);
             }
         };
     }
     
     @Override
-    public List<Widget> setupDisplay(Supplier<DefaultBeaconBaseDisplay> recipeDisplaySupplier, Rectangle bounds) {
-        DefaultBeaconBaseDisplay display = recipeDisplaySupplier.get();
+    public List<Widget> setupDisplay(DefaultBeaconBaseDisplay display, Rectangle bounds) {
         List<Widget> widgets = Lists.newArrayList();
-        widgets.add(EntryWidget.create(bounds.getCenterX() - 8, bounds.y + 3).entry(getLogo()));
+        widgets.add(Widgets.createSlot(new Point(bounds.getCenterX() - 8, bounds.y + 3)).entry(getLogo()));
         Rectangle rectangle = new Rectangle(bounds.getCenterX() - (bounds.width / 2) - 1, bounds.y + 23, bounds.width + 2, bounds.height - 28);
-        widgets.add(new SlotBaseWidget(rectangle));
+        widgets.add(Widgets.createSlotBase(rectangle));
         widgets.add(new ScrollableSlotsWidget(rectangle, CollectionUtils.map(display.getEntries(), t -> EntryWidget.create(0, 0).noBackground().entry(t))));
         return widgets;
     }
@@ -110,7 +113,7 @@ public class DefaultBeaconBaseCategory implements RecipeCategory<DefaultBeaconBa
     }
     
     private static class ScrollableSlotsWidget extends WidgetWithBounds {
-        private Rectangle bounds;
+        private me.shedaniel.math.api.Rectangle bounds;
         private List<EntryWidget> widgets;
         private double target;
         private double scroll;
@@ -118,7 +121,7 @@ public class DefaultBeaconBaseCategory implements RecipeCategory<DefaultBeaconBa
         private long duration;
         
         public ScrollableSlotsWidget(Rectangle bounds, List<EntryWidget> widgets) {
-            this.bounds = bounds;
+            this.bounds = new me.shedaniel.math.api.Rectangle(bounds);
             this.widgets = Lists.newArrayList(widgets);
         }
         
@@ -166,7 +169,7 @@ public class DefaultBeaconBaseCategory implements RecipeCategory<DefaultBeaconBa
         }
         
         @Override
-        public Rectangle getBounds() {
+        public me.shedaniel.math.api.Rectangle getBounds() {
             return bounds;
         }
         

+ 18 - 24
src/main/java/me/shedaniel/rei/plugin/brewing/DefaultBrewingCategory.java

@@ -23,12 +23,12 @@
 
 package me.shedaniel.rei.plugin.brewing;
 
-import me.shedaniel.math.api.Point;
-import me.shedaniel.math.api.Rectangle;
+import com.google.common.collect.Lists;
+import me.shedaniel.math.Point;
+import me.shedaniel.math.Rectangle;
 import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.RecipeCategory;
-import me.shedaniel.rei.gui.widget.EntryWidget;
-import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
+import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.plugin.DefaultPlugin;
 import net.minecraft.block.Blocks;
@@ -38,10 +38,7 @@ import net.minecraft.item.Items;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.math.MathHelper;
 
-import java.util.Collections;
-import java.util.LinkedList;
 import java.util.List;
-import java.util.function.Supplier;
 
 public class DefaultBrewingCategory implements RecipeCategory<DefaultBrewingDisplay> {
     
@@ -61,25 +58,22 @@ public class DefaultBrewingCategory implements RecipeCategory<DefaultBrewingDisp
     }
     
     @Override
-    public List<Widget> setupDisplay(Supplier<DefaultBrewingDisplay> recipeDisplaySupplier, Rectangle bounds) {
-        final DefaultBrewingDisplay recipeDisplay = recipeDisplaySupplier.get();
+    public List<Widget> setupDisplay(DefaultBrewingDisplay display, Rectangle bounds) {
         Point startPoint = new Point(bounds.getCenterX() - 52, bounds.getCenterY() - 29);
-        List<Widget> widgets = new LinkedList<>(Collections.singletonList(new RecipeBaseWidget(bounds) {
-            @Override
-            public void render(int mouseX, int mouseY, float delta) {
-                super.render(mouseX, mouseY, delta);
-                MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
-                blit(startPoint.x, startPoint.y, 0, 108, 103, 59);
-                int width = MathHelper.ceil(System.currentTimeMillis() / 250d % 18d);
-                blit(startPoint.x + 44, startPoint.y + 28, 103, 163, width, 4);
-            }
+        List<Widget> widgets = Lists.newArrayList();
+        widgets.add(Widgets.createRecipeBase(bounds));
+        widgets.add(Widgets.createDrawableWidget((helper, mouseX, mouseY, delta) -> {
+            MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
+            helper.blit(startPoint.x, startPoint.y, 0, 108, 103, 59);
+            int width = MathHelper.ceil(System.currentTimeMillis() / 250d % 18d);
+            helper.blit(startPoint.x + 44, startPoint.y + 28, 103, 163, width, 4);
         }));
-        widgets.add(EntryWidget.create(startPoint.x + 1, startPoint.y + 1).entry(EntryStack.create(Items.BLAZE_POWDER)).noBackground().markIsInput());
-        widgets.add(EntryWidget.create(startPoint.x + 40, startPoint.y + 1).entries(recipeDisplay.getInputEntries().get(0)).noBackground().markIsInput());
-        widgets.add(EntryWidget.create(startPoint.x + 63, startPoint.y + 1).entries(recipeDisplay.getInputEntries().get(1)).noBackground().markIsInput());
-        widgets.add(EntryWidget.create(startPoint.x + 40, startPoint.y + 35).entries(recipeDisplay.getOutput(0)).noBackground().markIsOutput());
-        widgets.add(EntryWidget.create(startPoint.x + 63, startPoint.y + 42).entries(recipeDisplay.getOutput(1)).noBackground().markIsOutput());
-        widgets.add(EntryWidget.create(startPoint.x + 86, startPoint.y + 35).entries(recipeDisplay.getOutput(2)).noBackground().markIsOutput());
+        widgets.add(Widgets.createSlot(new Point(startPoint.x + 1, startPoint.y + 1)).entry(EntryStack.create(Items.BLAZE_POWDER)).disableBackground().markInput());
+        widgets.add(Widgets.createSlot(new Point(startPoint.x + 40, startPoint.y + 1)).entries(display.getInputEntries().get(0)).disableBackground().markInput());
+        widgets.add(Widgets.createSlot(new Point(startPoint.x + 63, startPoint.y + 1)).entries(display.getInputEntries().get(1)).disableBackground().markInput());
+        widgets.add(Widgets.createSlot(new Point(startPoint.x + 40, startPoint.y + 35)).entries(display.getOutput(0)).disableBackground().markOutput());
+        widgets.add(Widgets.createSlot(new Point(startPoint.x + 63, startPoint.y + 42)).entries(display.getOutput(1)).disableBackground().markOutput());
+        widgets.add(Widgets.createSlot(new Point(startPoint.x + 86, startPoint.y + 35)).entries(display.getOutput(2)).disableBackground().markOutput());
         return widgets;
     }
     

+ 15 - 29
src/main/java/me/shedaniel/rei/plugin/campfire/DefaultCampfireCategory.java

@@ -23,27 +23,20 @@
 
 package me.shedaniel.rei.plugin.campfire;
 
-import me.shedaniel.math.api.Point;
-import me.shedaniel.math.api.Rectangle;
+import com.google.common.collect.Lists;
+import me.shedaniel.math.Point;
+import me.shedaniel.math.Rectangle;
 import me.shedaniel.rei.api.EntryStack;
-import me.shedaniel.rei.api.REIHelper;
 import me.shedaniel.rei.api.RecipeCategory;
-import me.shedaniel.rei.gui.widget.EntryWidget;
-import me.shedaniel.rei.gui.widget.RecipeArrowWidget;
-import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
+import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.plugin.DefaultPlugin;
 import net.minecraft.block.Blocks;
-import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.resource.language.I18n;
 import net.minecraft.util.Identifier;
-import net.minecraft.util.math.MathHelper;
 
 import java.text.DecimalFormat;
-import java.util.Collections;
-import java.util.LinkedList;
 import java.util.List;
-import java.util.function.Supplier;
 
 public class DefaultCampfireCategory implements RecipeCategory<DefaultCampfireDisplay> {
     
@@ -63,26 +56,19 @@ public class DefaultCampfireCategory implements RecipeCategory<DefaultCampfireDi
     }
     
     @Override
-    public List<Widget> setupDisplay(Supplier<DefaultCampfireDisplay> recipeDisplaySupplier, Rectangle bounds) {
+    public List<Widget> setupDisplay(DefaultCampfireDisplay display, Rectangle bounds) {
         Point startPoint = new Point(bounds.getCenterX() - 41, bounds.y + 10);
-        final double cookingTime = recipeDisplaySupplier.get().getCookTime();
+        final double cookingTime = display.getCookTime();
         DecimalFormat df = new DecimalFormat("###.##");
-        List<Widget> widgets = new LinkedList<>(Collections.singletonList(new RecipeBaseWidget(bounds) {
-            @Override
-            public void render(int mouseX, int mouseY, float delta) {
-                super.render(mouseX, mouseY, delta);
-                MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
-                blit(startPoint.x, startPoint.y, 0, 177, 82, 34);
-                int height = 14 - MathHelper.ceil(System.currentTimeMillis() / 250d % 14d);
-                blit(startPoint.x + 2, startPoint.y + 31 + (3 - height), 82, 77 + (14 - height), 14, height);
-                String text = I18n.translate("category.rei.campfire.time", df.format(cookingTime / 20d));
-                int length = MinecraftClient.getInstance().textRenderer.getStringWidth(text);
-                MinecraftClient.getInstance().textRenderer.draw(text, bounds.x + bounds.width - length - 5, bounds.y + 5, REIHelper.getInstance().isDarkThemeEnabled() ? 0xFFBBBBBB : 0xFF404040);
-            }
-        }));
-        widgets.add(RecipeArrowWidget.create(new Point(startPoint.x + 24, startPoint.y + 8), true).time(cookingTime * 50));
-        widgets.add(EntryWidget.create(startPoint.x + 1, startPoint.y + 1).entries(recipeDisplaySupplier.get().getInputEntries().get(0)).markIsInput());
-        widgets.add(EntryWidget.create(startPoint.x + 61, startPoint.y + 9).entries(recipeDisplaySupplier.get().getOutputEntries()).noBackground().markIsOutput());
+        List<Widget> widgets = Lists.newArrayList();
+        widgets.add(Widgets.createRecipeBase(bounds));
+        widgets.add(Widgets.createResultSlotBackground(new Point(startPoint.x + 61, startPoint.y + 9)));
+        widgets.add(Widgets.createBurningFire(new Point(startPoint.x + 1, startPoint.y + 20)).animationDurationMS(10000));
+        widgets.add(Widgets.createLabel(new Point(bounds.x + bounds.width - 5, bounds.y + 5),
+                I18n.translate("category.rei.campfire.time", df.format(cookingTime / 20d))).noShadow().rightAligned().color(0xFF404040, 0xFFBBBBBB));
+        widgets.add(Widgets.createArrow(new Point(startPoint.x + 24, startPoint.y + 8)).animationDurationTicks(cookingTime));
+        widgets.add(Widgets.createSlot(new Point(startPoint.x + 1, startPoint.y + 1)).entries(display.getInputEntries().get(0)).markInput());
+        widgets.add(Widgets.createSlot(new Point(startPoint.x + 61, startPoint.y + 9)).entries(display.getOutputEntries()).disableBackground().markOutput());
         return widgets;
     }
     

+ 13 - 19
src/main/java/me/shedaniel/rei/plugin/composting/DefaultCompostingCategory.java

@@ -24,14 +24,14 @@
 package me.shedaniel.rei.plugin.composting;
 
 import com.google.common.collect.Lists;
-import me.shedaniel.math.api.Point;
-import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.math.Point;
+import me.shedaniel.math.Rectangle;
 import me.shedaniel.rei.api.EntryStack;
+import me.shedaniel.rei.api.REIHelper;
 import me.shedaniel.rei.api.RecipeCategory;
+import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.entries.RecipeEntry;
-import me.shedaniel.rei.gui.widget.EntryWidget;
 import me.shedaniel.rei.gui.widget.QueuedTooltip;
-import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
 import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.plugin.DefaultPlugin;
 import net.minecraft.block.Blocks;
@@ -46,7 +46,6 @@ import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import java.util.function.Supplier;
 
 public class DefaultCompostingCategory implements RecipeCategory<DefaultCompostingDisplay> {
     
@@ -80,41 +79,36 @@ public class DefaultCompostingCategory implements RecipeCategory<DefaultComposti
             }
             
             @Override
-            public void render(Rectangle rectangle, int mouseX, int mouseY, float delta) {
+            public void render(me.shedaniel.math.api.Rectangle rectangle, int mouseX, int mouseY, float delta) {
                 MinecraftClient.getInstance().textRenderer.draw(I18n.translate("text.rei.composting.page", recipe.getPage() + 1), rectangle.x + 5, rectangle.y + 6, -1);
             }
         };
     }
     
     @Override
-    public List<Widget> setupDisplay(Supplier<DefaultCompostingDisplay> recipeDisplaySupplier, Rectangle bounds) {
+    public List<Widget> setupDisplay(DefaultCompostingDisplay display, Rectangle bounds) {
         List<Widget> widgets = Lists.newArrayList();
         Point startingPoint = new Point(bounds.x + bounds.width - 55, bounds.y + 110);
-        widgets.add(new RecipeBaseWidget(bounds) {
-            @Override
-            public void render(int mouseX, int mouseY, float partialTicks) {
-                fillGradient(getBounds().x, getBounds().y, getBounds().getMaxX(), getBounds().getMaxY(), getInnerColor(), getInnerColor());
-                MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
-                this.blit(startingPoint.x, startingPoint.y + 3, 28, 221, 55, 26);
-            }
-        });
-        List<EntryStack> stacks = new LinkedList<>(recipeDisplaySupplier.get().getItemsByOrder());
+        widgets.add(Widgets.createFilledRectangle(bounds, REIHelper.getInstance().isDarkThemeEnabled() ? -13750738 : -3750202));
+        List<EntryStack> stacks = new LinkedList<>(display.getItemsByOrder());
         int i = 0;
         for (int y = 0; y < 6; y++)
             for (int x = 0; x < 8; x++) {
                 int finalI = i;
                 EntryStack entryStack = stacks.size() > i ? stacks.get(finalI) : EntryStack.empty();
                 if (entryStack.getType() != EntryStack.Type.EMPTY)
-                    for (Map.Entry<ItemConvertible, Float> entry : recipeDisplaySupplier.get().getInputMap().entrySet()) {
+                    for (Map.Entry<ItemConvertible, Float> entry : display.getInputMap().entrySet()) {
                         if (entry.getKey().asItem().equals(entryStack.getItem())) {
                             entryStack = entryStack.setting(EntryStack.Settings.TOOLTIP_APPEND_EXTRA, s -> Collections.singletonList(I18n.translate("text.rei.composting.chance", MathHelper.fastFloor(entry.getValue() * 100))));
                             break;
                         }
                     }
-                widgets.add(EntryWidget.create(bounds.getCenterX() - 72 + x * 18, bounds.y + 3 + y * 18).entry(entryStack).markIsInput());
+                widgets.add(Widgets.createSlot(new Point(bounds.getCenterX() - 72 + x * 18, bounds.y + 3 + y * 18)).entry(entryStack).markInput());
                 i++;
             }
-        widgets.add(EntryWidget.create(startingPoint.x + 33, startingPoint.y + 8).entries(recipeDisplaySupplier.get().getOutputEntries()).noBackground().markIsOutput());
+        widgets.add(Widgets.createArrow(new Point(startingPoint.x - 1, startingPoint.y + 7)));
+        widgets.add(Widgets.createResultSlotBackground(new Point(startingPoint.x + 33, startingPoint.y + 8)));
+        widgets.add(Widgets.createSlot(new Point(startingPoint.x + 33, startingPoint.y + 8)).entries(display.getOutputEntries()).disableBackground().markOutput());
         return widgets;
     }
     

+ 16 - 31
src/main/java/me/shedaniel/rei/plugin/cooking/DefaultCookingCategory.java

@@ -23,31 +23,24 @@
 
 package me.shedaniel.rei.plugin.cooking;
 
+import com.google.common.collect.Lists;
 import com.mojang.blaze3d.systems.RenderSystem;
 import it.unimi.dsi.fastutil.ints.IntList;
-import me.shedaniel.math.api.Point;
-import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.math.Point;
+import me.shedaniel.math.Rectangle;
 import me.shedaniel.rei.api.EntryStack;
-import me.shedaniel.rei.api.REIHelper;
 import me.shedaniel.rei.api.TransferRecipeCategory;
+import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.entries.RecipeEntry;
 import me.shedaniel.rei.gui.entries.SimpleRecipeEntry;
-import me.shedaniel.rei.gui.widget.EntryWidget;
-import me.shedaniel.rei.gui.widget.RecipeArrowWidget;
-import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
 import me.shedaniel.rei.gui.widget.Widget;
-import me.shedaniel.rei.plugin.DefaultPlugin;
-import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.DrawableHelper;
 import net.minecraft.client.resource.language.I18n;
 import net.minecraft.util.Identifier;
-import net.minecraft.util.math.MathHelper;
 
 import java.text.DecimalFormat;
 import java.util.Collections;
-import java.util.LinkedList;
 import java.util.List;
-import java.util.function.Supplier;
 
 public class DefaultCookingCategory implements TransferRecipeCategory<DefaultCookingDisplay> {
     private Identifier identifier;
@@ -61,7 +54,7 @@ public class DefaultCookingCategory implements TransferRecipeCategory<DefaultCoo
     }
     
     @Override
-    public void renderRedSlots(List<Widget> widgets, Rectangle bounds, DefaultCookingDisplay display, IntList redSlots) {
+    public void renderRedSlots(List<Widget> widgets, me.shedaniel.math.api.Rectangle bounds, DefaultCookingDisplay display, IntList redSlots) {
         Point startPoint = new Point(bounds.getCenterX() - 41, bounds.getCenterY() - 27);
         RenderSystem.translatef(0, 0, 400);
         if (redSlots.contains(0)) {
@@ -71,27 +64,19 @@ public class DefaultCookingCategory implements TransferRecipeCategory<DefaultCoo
     }
     
     @Override
-    public List<Widget> setupDisplay(Supplier<DefaultCookingDisplay> recipeDisplaySupplier, Rectangle bounds) {
+    public List<Widget> setupDisplay(DefaultCookingDisplay display, Rectangle bounds) {
         Point startPoint = new Point(bounds.getCenterX() - 41, bounds.y + 10);
-        double cookingTime = recipeDisplaySupplier.get().getCookingTime();
+        double cookingTime = display.getCookingTime();
         DecimalFormat df = new DecimalFormat("###.##");
-        List<Widget> widgets = new LinkedList<>(Collections.singletonList(new RecipeBaseWidget(bounds) {
-            @Override
-            public void render(int mouseX, int mouseY, float delta) {
-                super.render(mouseX, mouseY, delta);
-                MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
-                blit(startPoint.x, startPoint.y, 0, 177, 82, 34);
-                int height = 14 - MathHelper.ceil((System.currentTimeMillis() / 250d % 14d) / 1f);
-                blit(startPoint.x + 2, startPoint.y + 31 + (3 - height), 82, 77 + (14 - height), 14, height);
-                String text = I18n.translate("category.rei.cooking.time&xp", df.format(recipeDisplaySupplier.get().getXp()), df.format(cookingTime / 20d));
-                int length = MinecraftClient.getInstance().textRenderer.getStringWidth(text);
-                MinecraftClient.getInstance().textRenderer.draw(text, bounds.x + bounds.width - length - 5, bounds.y + 5, REIHelper.getInstance().isDarkThemeEnabled() ? 0xFFBBBBBB : 0xFF404040);
-                
-            }
-        }));
-        widgets.add(RecipeArrowWidget.create(new Point(startPoint.x + 24, startPoint.y + 8), true).time(cookingTime * 50));
-        widgets.add(EntryWidget.create(startPoint.x + 1, startPoint.y + 1).entries(recipeDisplaySupplier.get().getInputEntries().get(0)).markIsInput());
-        widgets.add(EntryWidget.create(startPoint.x + 61, startPoint.y + 9).entries(recipeDisplaySupplier.get().getOutputEntries()).noBackground().markIsOutput());
+        List<Widget> widgets = Lists.newArrayList();
+        widgets.add(Widgets.createRecipeBase(bounds));
+        widgets.add(Widgets.createResultSlotBackground(new Point(startPoint.x + 61, startPoint.y + 9)));
+        widgets.add(Widgets.createBurningFire(new Point(startPoint.x + 1, startPoint.y + 20)).animationDurationMS(10000));
+        widgets.add(Widgets.createLabel(new Point(bounds.x + bounds.width - 5, bounds.y + 5),
+                I18n.translate("category.rei.cooking.time&xp", df.format(display.getXp()), df.format(cookingTime / 20d))).noShadow().rightAligned().color(0xFF404040, 0xFFBBBBBB));
+        widgets.add(Widgets.createArrow(new Point(startPoint.x + 24, startPoint.y + 8)).animationDurationTicks(cookingTime));
+        widgets.add(Widgets.createSlot(new Point(startPoint.x + 1, startPoint.y + 1)).entries(display.getInputEntries().get(0)).markInput());
+        widgets.add(Widgets.createSlot(new Point(startPoint.x + 61, startPoint.y + 9)).entries(display.getOutputEntries()).disableBackground().markOutput());
         return widgets;
     }
     

+ 16 - 23
src/main/java/me/shedaniel/rei/plugin/crafting/DefaultCraftingCategory.java

@@ -26,28 +26,25 @@ package me.shedaniel.rei.plugin.crafting;
 import com.google.common.collect.Lists;
 import com.mojang.blaze3d.systems.RenderSystem;
 import it.unimi.dsi.fastutil.ints.IntList;
-import me.shedaniel.math.api.Point;
-import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.math.Point;
+import me.shedaniel.math.Rectangle;
 import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.TransferRecipeCategory;
-import me.shedaniel.rei.gui.widget.EntryWidget;
-import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
+import me.shedaniel.rei.api.widgets.Slot;
+import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.impl.ScreenHelper;
 import me.shedaniel.rei.plugin.DefaultPlugin;
 import me.shedaniel.rei.server.ContainerInfo;
 import me.shedaniel.rei.server.ContainerInfoHandler;
 import net.minecraft.block.Blocks;
-import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.DrawableHelper;
 import net.minecraft.client.resource.language.I18n;
 import net.minecraft.container.Container;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.math.MathHelper;
 
-import java.util.Collections;
 import java.util.List;
-import java.util.function.Supplier;
 
 public class DefaultCraftingCategory implements TransferRecipeCategory<DefaultCraftingDisplay> {
     
@@ -73,35 +70,31 @@ public class DefaultCraftingCategory implements TransferRecipeCategory<DefaultCr
     }
     
     @Override
-    public List<Widget> setupDisplay(Supplier<DefaultCraftingDisplay> recipeDisplaySupplier, Rectangle bounds) {
+    public List<Widget> setupDisplay(DefaultCraftingDisplay display, Rectangle bounds) {
         Point startPoint = new Point(bounds.getCenterX() - 58, bounds.getCenterY() - 27);
-        List<Widget> widgets = Lists.newLinkedList(Collections.singletonList(new RecipeBaseWidget(bounds) {
-            @Override
-            public void render(int mouseX, int mouseY, float delta) {
-                super.render(mouseX, mouseY, delta);
-                MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
-                blit(startPoint.x, startPoint.y, 0, 0, 116, 54);
-            }
-        }));
-        List<List<EntryStack>> input = recipeDisplaySupplier.get().getInputEntries();
-        List<EntryWidget> slots = Lists.newArrayList();
+        List<Widget> widgets = Lists.newArrayList();
+        widgets.add(Widgets.createRecipeBase(bounds));
+        widgets.add(Widgets.createArrow(new Point(startPoint.x + 60, startPoint.y + 18)));
+        widgets.add(Widgets.createResultSlotBackground(new Point(startPoint.x + 95, startPoint.y + 19)));
+        List<List<EntryStack>> input = display.getInputEntries();
+        List<Slot> slots = Lists.newArrayList();
         for (int y = 0; y < 3; y++)
             for (int x = 0; x < 3; x++)
-                slots.add(EntryWidget.create(startPoint.x + 1 + x * 18, startPoint.y + 1 + y * 18).markIsInput());
+                slots.add(Widgets.createSlot(new Point(startPoint.x + 1 + x * 18, startPoint.y + 1 + y * 18)).markInput());
         for (int i = 0; i < input.size(); i++) {
-            if (recipeDisplaySupplier.get() instanceof DefaultShapedDisplay) {
+            if (display instanceof DefaultShapedDisplay) {
                 if (!input.get(i).isEmpty())
-                    slots.get(getSlotWithSize(recipeDisplaySupplier.get(), i, 3)).entries(input.get(i));
+                    slots.get(getSlotWithSize(display, i, 3)).entries(input.get(i));
             } else if (!input.get(i).isEmpty())
                 slots.get(i).entries(input.get(i));
         }
         widgets.addAll(slots);
-        widgets.add(EntryWidget.create(startPoint.x + 95, startPoint.y + 19).entries(recipeDisplaySupplier.get().getOutputEntries()).noBackground().markIsOutput());
+        widgets.add(Widgets.createSlot(new Point(startPoint.x + 95, startPoint.y + 19)).entries(display.getOutputEntries()).disableBackground().markOutput());
         return widgets;
     }
     
     @Override
-    public void renderRedSlots(List<Widget> widgets, Rectangle bounds, DefaultCraftingDisplay display, IntList redSlots) {
+    public void renderRedSlots(List<Widget> widgets, me.shedaniel.math.api.Rectangle bounds, DefaultCraftingDisplay display, IntList redSlots) {
         ContainerInfo<Container> info = (ContainerInfo<Container>) ContainerInfoHandler.getContainerInfo(getIdentifier(), ScreenHelper.getLastContainerScreen().getContainer().getClass());
         if (info == null)
             return;

+ 19 - 30
src/main/java/me/shedaniel/rei/plugin/fuel/DefaultFuelCategory.java

@@ -23,29 +23,24 @@
 
 package me.shedaniel.rei.plugin.fuel;
 
-import me.shedaniel.math.api.Point;
-import me.shedaniel.math.api.Rectangle;
+import com.google.common.collect.Lists;
+import me.shedaniel.math.Point;
 import me.shedaniel.rei.api.EntryStack;
-import me.shedaniel.rei.api.REIHelper;
 import me.shedaniel.rei.api.RecipeCategory;
+import me.shedaniel.rei.api.widgets.Slot;
+import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.entries.RecipeEntry;
-import me.shedaniel.rei.gui.widget.EntryWidget;
 import me.shedaniel.rei.gui.widget.QueuedTooltip;
-import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
 import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.plugin.DefaultPlugin;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.resource.language.I18n;
 import net.minecraft.item.Items;
 import net.minecraft.util.Identifier;
-import net.minecraft.util.math.MathHelper;
 import org.jetbrains.annotations.Nullable;
 
 import java.text.DecimalFormat;
-import java.util.Collections;
-import java.util.LinkedList;
 import java.util.List;
-import java.util.function.Supplier;
 
 public class DefaultFuelCategory implements RecipeCategory<DefaultFuelDisplay> {
     
@@ -72,27 +67,21 @@ public class DefaultFuelCategory implements RecipeCategory<DefaultFuelDisplay> {
     }
     
     @Override
-    public List<Widget> setupDisplay(Supplier<DefaultFuelDisplay> recipeDisplaySupplier, Rectangle bounds) {
+    public List<Widget> setupDisplay(DefaultFuelDisplay recipeDisplay, me.shedaniel.math.Rectangle bounds) {
         Point startPoint = new Point(bounds.getCenterX() - 41, bounds.getCenterY() - 17);
-        String burnItems = DECIMAL_FORMAT.format(recipeDisplaySupplier.get().getFuelTime() / 200d);
-        List<Widget> widgets = new LinkedList<>(Collections.singletonList(new RecipeBaseWidget(bounds) {
-            @Override
-            public void render(int mouseX, int mouseY, float delta) {
-                super.render(mouseX, mouseY, delta);
-                MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
-                blit(bounds.x + 5, startPoint.y, 0, 73, 18, 34);
-                int height = MathHelper.ceil(System.currentTimeMillis() / 250d % 14d);
-                blit(bounds.x + 7, startPoint.y + 12 + (3 - height), 82, 77 + (14 - height), 14, height);
-                minecraft.textRenderer.draw(I18n.translate("category.rei.fuel.time.items", burnItems), bounds.x + 26, bounds.getMaxY() - 15, REIHelper.getInstance().isDarkThemeEnabled() ? 0xFFBBBBBB : 0xFF404040);
-            }
-        }));
-        widgets.add(EntryWidget.create(bounds.x + 6, startPoint.y + 18).entries(recipeDisplaySupplier.get().getInputEntries().get(0)).markIsInput());
+        String burnItems = DECIMAL_FORMAT.format(recipeDisplay.getFuelTime() / 200d);
+        List<Widget> widgets = Lists.newArrayList();
+        widgets.add(Widgets.createRecipeBase(bounds));
+        widgets.add(Widgets.createLabel(new Point(bounds.x + 26, bounds.getMaxY() - 15), I18n.translate("category.rei.fuel.time.items", burnItems))
+                .color(0xFF404040, 0xFFBBBBBB).noShadow().leftAligned());
+        widgets.add(Widgets.createBurningFire(new Point(bounds.x + 6, startPoint.y + 1)).animationDurationTicks(recipeDisplay.getFuelTime()));
+        widgets.add(Widgets.createSlot(new Point(bounds.x + 6, startPoint.y + 18)).entries(recipeDisplay.getInputEntries().get(0)).markInput());
         return widgets;
     }
     
     @Override
     public RecipeEntry getSimpleRenderer(DefaultFuelDisplay recipe) {
-        EntryWidget widget = EntryWidget.create(0, 0).entries(recipe.getInputEntries().get(0)).noBackground().noHighlight();
+        Slot slot = Widgets.createSlot(new Point(0, 0)).entries(recipe.getInputEntries().get(0)).disableBackground().disableHighlight();
         String burnItems = DECIMAL_FORMAT.format(recipe.getFuelTime() / 200d);
         return new RecipeEntry() {
             @Override
@@ -103,16 +92,16 @@ public class DefaultFuelCategory implements RecipeCategory<DefaultFuelDisplay> {
             @Nullable
             @Override
             public QueuedTooltip getTooltip(int mouseX, int mouseY) {
-                if (widget.containsMouse(mouseX, mouseY))
-                    return widget.getCurrentTooltip(mouseX, mouseY);
+                if (slot.containsMouse(mouseX, mouseY))
+                    return slot.getCurrentTooltip(mouseX, mouseY);
                 return null;
             }
             
             @Override
-            public void render(Rectangle bounds, int mouseX, int mouseY, float delta) {
-                widget.setZ(getZ() + 50);
-                widget.getBounds().setLocation(bounds.x + 4, bounds.y + 2);
-                widget.render(mouseX, mouseY, delta);
+            public void render(me.shedaniel.math.api.Rectangle bounds, int mouseX, int mouseY, float delta) {
+                slot.setZ(getZ() + 50);
+                slot.getBounds().setLocation(bounds.x + 4, bounds.y + 2);
+                slot.render(mouseX, mouseY, delta);
                 MinecraftClient.getInstance().textRenderer.drawWithShadow(I18n.translate("category.rei.fuel.time_short.items", burnItems), bounds.x + 25, bounds.y + 8, -1);
             }
         };

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

@@ -28,14 +28,17 @@ import com.mojang.blaze3d.systems.RenderSystem;
 import me.shedaniel.clothconfig2.ClothConfigInitializer;
 import me.shedaniel.clothconfig2.api.ScissorsHandler;
 import me.shedaniel.clothconfig2.gui.widget.DynamicEntryListWidget;
-import me.shedaniel.math.api.Point;
-import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.math.Point;
+import me.shedaniel.math.Rectangle;
 import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.REIHelper;
 import me.shedaniel.rei.api.RecipeCategory;
+import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.entries.RecipeEntry;
-import me.shedaniel.rei.gui.widget.*;
+import me.shedaniel.rei.gui.widget.QueuedTooltip;
+import me.shedaniel.rei.gui.widget.Widget;
+import me.shedaniel.rei.gui.widget.WidgetWithBounds;
 import me.shedaniel.rei.impl.RenderingEntry;
 import me.shedaniel.rei.plugin.DefaultPlugin;
 import net.minecraft.client.MinecraftClient;
@@ -54,7 +57,6 @@ import org.jetbrains.annotations.Nullable;
 
 import java.util.Collections;
 import java.util.List;
-import java.util.function.Supplier;
 
 public class DefaultInformationCategory implements RecipeCategory<DefaultInformationDisplay> {
     protected static void innerBlit(Matrix4f matrix4f, int xStart, int xEnd, int yStart, int yEnd, int z, float uStart, float uEnd, float vStart, float vEnd) {
@@ -95,7 +97,7 @@ public class DefaultInformationCategory implements RecipeCategory<DefaultInforma
             }
             
             @Override
-            public void render(Rectangle rectangle, int mouseX, int mouseY, float delta) {
+            public void render(me.shedaniel.math.api.Rectangle rectangle, int mouseX, int mouseY, float delta) {
                 MinecraftClient.getInstance().textRenderer.draw(name.asFormattedString(), rectangle.x + 5, rectangle.y + 6, -1);
             }
         };
@@ -105,7 +107,7 @@ public class DefaultInformationCategory implements RecipeCategory<DefaultInforma
     public EntryStack getLogo() {
         return new RenderingEntry() {
             @Override
-            public void render(Rectangle bounds, int mouseX, int mouseY, float delta) {
+            public void render(me.shedaniel.math.api.Rectangle bounds, int mouseX, int mouseY, float delta) {
                 MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
                 Matrix4f matrix4f = Matrix4f.translate(-1.2f, -1, 0);
                 DefaultInformationCategory.innerBlit(matrix4f, bounds.getCenterX() - 8, bounds.getCenterX() + 8, bounds.getCenterY() - 8, bounds.getCenterY() + 8, 0, 116f / 256f, (116f + 16f) / 256f, 0f, 16f / 256f);
@@ -114,14 +116,13 @@ public class DefaultInformationCategory implements RecipeCategory<DefaultInforma
     }
     
     @Override
-    public List<Widget> setupDisplay(Supplier<DefaultInformationDisplay> recipeDisplaySupplier, Rectangle bounds) {
-        DefaultInformationDisplay display = recipeDisplaySupplier.get();
+    public List<Widget> setupDisplay(DefaultInformationDisplay recipeDisplay, me.shedaniel.math.Rectangle bounds) {
         List<Widget> widgets = Lists.newArrayList();
-        widgets.add(LabelWidget.create(new Point(bounds.getCenterX(), bounds.y + 3), display.getName().asFormattedString()).noShadow().color(REIHelper.getInstance().isDarkThemeEnabled() ? 0xFFBBBBBB : 0xFF404040));
-        widgets.add(EntryWidget.create(bounds.getCenterX() - 8, bounds.y + 15).entries(display.getEntryStacks()).markIsInput());
+        widgets.add(Widgets.createLabel(new Point(bounds.getCenterX(), bounds.y + 3), recipeDisplay.getName().asFormattedString()).noShadow().color(0xFF404040, 0xFFBBBBBB));
+        widgets.add(Widgets.createSlot(new Point(bounds.getCenterX() - 8, bounds.y + 15)).entries(recipeDisplay.getEntryStacks()).markInput());
         Rectangle rectangle = new Rectangle(bounds.getCenterX() - (bounds.width / 2), bounds.y + 35, bounds.width, bounds.height - 40);
-        widgets.add(new SlotBaseWidget(rectangle));
-        widgets.add(new ScrollableTextWidget(rectangle, display.getTexts()));
+        widgets.add(Widgets.createSlotBase(rectangle));
+        widgets.add(new ScrollableTextWidget(rectangle, recipeDisplay.getTexts()));
         return widgets;
     }
     
@@ -136,7 +137,7 @@ public class DefaultInformationCategory implements RecipeCategory<DefaultInforma
     }
     
     private static class ScrollableTextWidget extends WidgetWithBounds {
-        private Rectangle bounds;
+        private me.shedaniel.math.api.Rectangle bounds;
         private List<Text> texts;
         private double target;
         private double scroll;
@@ -144,7 +145,7 @@ public class DefaultInformationCategory implements RecipeCategory<DefaultInforma
         private long duration;
         
         public ScrollableTextWidget(Rectangle bounds, List<Text> texts) {
-            this.bounds = bounds;
+            this.bounds = new me.shedaniel.math.api.Rectangle(bounds);
             this.texts = Lists.newArrayList();
             for (Text text : texts) {
                 if (!this.texts.isEmpty())
@@ -201,7 +202,7 @@ public class DefaultInformationCategory implements RecipeCategory<DefaultInforma
         }
         
         @Override
-        public Rectangle getBounds() {
+        public me.shedaniel.math.api.Rectangle getBounds() {
             return bounds;
         }
         

+ 11 - 18
src/main/java/me/shedaniel/rei/plugin/stonecutting/DefaultStoneCuttingCategory.java

@@ -23,23 +23,20 @@
 
 package me.shedaniel.rei.plugin.stonecutting;
 
-import me.shedaniel.math.api.Point;
-import me.shedaniel.math.api.Rectangle;
+import com.google.common.collect.Lists;
+import me.shedaniel.math.Point;
+import me.shedaniel.math.Rectangle;
 import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.RecipeCategory;
+import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.widget.EntryWidget;
-import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
 import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.plugin.DefaultPlugin;
 import net.minecraft.block.Blocks;
-import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.resource.language.I18n;
 import net.minecraft.util.Identifier;
 
-import java.util.Collections;
-import java.util.LinkedList;
 import java.util.List;
-import java.util.function.Supplier;
 
 public class DefaultStoneCuttingCategory implements RecipeCategory<DefaultStoneCuttingDisplay> {
     
@@ -59,18 +56,14 @@ public class DefaultStoneCuttingCategory implements RecipeCategory<DefaultStoneC
     }
     
     @Override
-    public List<Widget> setupDisplay(Supplier<DefaultStoneCuttingDisplay> recipeDisplaySupplier, Rectangle bounds) {
+    public List<Widget> setupDisplay(DefaultStoneCuttingDisplay display, Rectangle bounds) {
         Point startPoint = new Point(bounds.getCenterX() - 41, bounds.getCenterY() - 13);
-        List<Widget> widgets = new LinkedList<>(Collections.singletonList(new RecipeBaseWidget(bounds) {
-            @Override
-            public void render(int mouseX, int mouseY, float delta) {
-                super.render(mouseX, mouseY, delta);
-                MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
-                blit(startPoint.x, startPoint.y, 0, 221, 82, 26);
-            }
-        }));
-        widgets.add(EntryWidget.create(startPoint.x + 4, startPoint.y + 5).entries(recipeDisplaySupplier.get().getInputEntries().get(0)).noBackground().markIsInput());
-        widgets.add(EntryWidget.create(startPoint.x + 61, startPoint.y + 5).entries(recipeDisplaySupplier.get().getOutputEntries()).noBackground().markIsOutput());
+        List<Widget> widgets = Lists.newArrayList();
+        widgets.add(Widgets.createRecipeBase(bounds));
+        widgets.add(Widgets.createArrow(new Point(startPoint.x + 27, startPoint.y + 4)));
+        widgets.add(Widgets.createResultSlotBackground(new Point(startPoint.x + 61, startPoint.y + 5)));
+        widgets.add(Widgets.createSlot(new Point(startPoint.x + 4, startPoint.y + 5)).entries(display.getInputEntries().get(0)).markInput());
+        widgets.add(Widgets.createSlot(new Point(startPoint.x + 61, startPoint.y + 5)).entries(display.getOutputEntries()).disableBackground().markOutput());
         return widgets;
     }
     

+ 11 - 19
src/main/java/me/shedaniel/rei/plugin/stripping/DefaultStrippingCategory.java

@@ -23,23 +23,19 @@
 
 package me.shedaniel.rei.plugin.stripping;
 
-import me.shedaniel.math.api.Point;
-import me.shedaniel.math.api.Rectangle;
+import com.google.common.collect.Lists;
+import me.shedaniel.math.Point;
+import me.shedaniel.math.Rectangle;
 import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.api.RecipeCategory;
-import me.shedaniel.rei.gui.widget.EntryWidget;
-import me.shedaniel.rei.gui.widget.RecipeBaseWidget;
+import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.plugin.DefaultPlugin;
-import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.resource.language.I18n;
 import net.minecraft.item.Items;
 import net.minecraft.util.Identifier;
 
-import java.util.Collections;
-import java.util.LinkedList;
 import java.util.List;
-import java.util.function.Supplier;
 
 public class DefaultStrippingCategory implements RecipeCategory<DefaultStrippingDisplay> {
     
@@ -59,18 +55,14 @@ public class DefaultStrippingCategory implements RecipeCategory<DefaultStripping
     }
     
     @Override
-    public List<Widget> setupDisplay(Supplier<DefaultStrippingDisplay> recipeDisplaySupplier, Rectangle bounds) {
+    public List<Widget> setupDisplay(DefaultStrippingDisplay recipeDisplay, Rectangle bounds) {
         Point startPoint = new Point(bounds.getCenterX() - 41, bounds.getCenterY() - 13);
-        List<Widget> widgets = new LinkedList<>(Collections.singletonList(new RecipeBaseWidget(bounds) {
-            @Override
-            public void render(int mouseX, int mouseY, float delta) {
-                super.render(mouseX, mouseY, delta);
-                MinecraftClient.getInstance().getTextureManager().bindTexture(DefaultPlugin.getDisplayTexture());
-                blit(startPoint.x, startPoint.y, 0, 221, 82, 26);
-            }
-        }));
-        widgets.add(EntryWidget.create(startPoint.x + 4, startPoint.y + 5).entry(recipeDisplaySupplier.get().getIn()).markIsInput());
-        widgets.add(EntryWidget.create(startPoint.x + 61, startPoint.y + 5).entry(recipeDisplaySupplier.get().getOut()).noBackground().markIsOutput());
+        List<Widget> widgets = Lists.newArrayList();
+        widgets.add(Widgets.createRecipeBase(bounds));
+        widgets.add(Widgets.createArrow(new Point(startPoint.x + 27, startPoint.y + 4)));
+        widgets.add(Widgets.createResultSlotBackground(new Point(startPoint.x + 61, startPoint.y + 5)));
+        widgets.add(Widgets.createSlot(new Point(startPoint.x + 4, startPoint.y + 5)).entry(recipeDisplay.getIn()).markInput());
+        widgets.add(Widgets.createSlot(new Point(startPoint.x + 61, startPoint.y + 5)).entry(recipeDisplay.getOut()).disableBackground().markInput());
         return widgets;
     }
     

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

@@ -26,13 +26,11 @@ package me.shedaniel.rei.utils;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import me.shedaniel.rei.api.EntryStack;
-import org.jetbrains.annotations.ApiStatus;
 
 import java.util.*;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
-@ApiStatus.Internal
 public class CollectionUtils {
     public static <A, B> List<B> getOrPutEmptyList(Map<A, List<B>> map, A key) {
         List<B> b = map.get(key);