Эх сурвалжийг харах

4.0.14: Better widgets system

Signed-off-by: shedaniel <daniel@shedaniel.me>
shedaniel 5 жил өмнө
parent
commit
f132558108
40 өөрчлөгдсөн 1270 нэмэгдсэн , 615 устгасан
  1. 1 1
      gradle.properties
  2. 16 6
      src/main/java/me/shedaniel/rei/api/widgets/Arrow.java
  3. 30 0
      src/main/java/me/shedaniel/rei/api/widgets/BaseWidget.java
  4. 16 6
      src/main/java/me/shedaniel/rei/api/widgets/BurningFire.java
  5. 160 0
      src/main/java/me/shedaniel/rei/api/widgets/Button.java
  6. 112 0
      src/main/java/me/shedaniel/rei/api/widgets/Label.java
  7. 7 0
      src/main/java/me/shedaniel/rei/api/widgets/Panel.java
  8. 30 2
      src/main/java/me/shedaniel/rei/api/widgets/Slot.java
  9. 5 0
      src/main/java/me/shedaniel/rei/api/widgets/Tooltip.java
  10. 43 0
      src/main/java/me/shedaniel/rei/api/widgets/Widgets.java
  11. 127 146
      src/main/java/me/shedaniel/rei/gui/ContainerScreenOverlay.java
  12. 8 14
      src/main/java/me/shedaniel/rei/gui/PreRecipeViewingScreen.java
  13. 72 60
      src/main/java/me/shedaniel/rei/gui/RecipeViewingScreen.java
  14. 44 60
      src/main/java/me/shedaniel/rei/gui/VillagerRecipeViewingScreen.java
  15. 0 192
      src/main/java/me/shedaniel/rei/gui/widget/AutoCraftingButtonWidget.java
  16. 9 0
      src/main/java/me/shedaniel/rei/gui/widget/ButtonWidget.java
  17. 0 82
      src/main/java/me/shedaniel/rei/gui/widget/CraftableToggleButtonWidget.java
  18. 2 0
      src/main/java/me/shedaniel/rei/gui/widget/EntryListWidget.java
  19. 8 0
      src/main/java/me/shedaniel/rei/gui/widget/EntryWidget.java
  20. 2 0
      src/main/java/me/shedaniel/rei/gui/widget/FavoritesListWidget.java
  21. 2 0
      src/main/java/me/shedaniel/rei/gui/widget/LabelWidget.java
  22. 0 35
      src/main/java/me/shedaniel/rei/gui/widget/LateRenderedButton.java
  23. 2 0
      src/main/java/me/shedaniel/rei/gui/widget/PanelWidget.java
  24. 10 0
      src/main/java/me/shedaniel/rei/gui/widget/QueuedTooltip.java
  25. 2 0
      src/main/java/me/shedaniel/rei/gui/widget/RecipeArrowWidget.java
  26. 10 6
      src/main/java/me/shedaniel/rei/gui/widget/RecipeChoosePageWidget.java
  27. 2 0
      src/main/java/me/shedaniel/rei/gui/widget/TabWidget.java
  28. 7 0
      src/main/java/me/shedaniel/rei/gui/widget/TextFieldWidget.java
  29. 2 0
      src/main/java/me/shedaniel/rei/gui/widget/WidgetWithBounds.java
  30. 242 0
      src/main/java/me/shedaniel/rei/impl/InternalWidgets.java
  31. 3 0
      src/main/java/me/shedaniel/rei/impl/ScreenHelper.java
  32. 3 2
      src/main/java/me/shedaniel/rei/impl/widgets/ArrowWidget.java
  33. 1 1
      src/main/java/me/shedaniel/rei/impl/widgets/BurningFireWidget.java
  34. 284 0
      src/main/java/me/shedaniel/rei/impl/widgets/ButtonWidget.java
  35. 1 1
      src/main/java/me/shedaniel/rei/impl/widgets/FillRectangleDrawableConsumer.java
  36. 1 0
      src/main/java/me/shedaniel/rei/impl/widgets/LabelWidget.java
  37. 1 0
      src/main/java/me/shedaniel/rei/impl/widgets/PanelWidget.java
  38. 1 1
      src/main/java/me/shedaniel/rei/impl/widgets/TexturedDrawableConsumer.java
  39. 2 0
      src/main/java/me/shedaniel/rei/plugin/beacon/DefaultBeaconBaseCategory.java
  40. 2 0
      src/main/java/me/shedaniel/rei/plugin/information/DefaultInformationCategory.java

+ 1 - 1
gradle.properties

@@ -1,4 +1,4 @@
-mod_version=4.0.13-unstable
+mod_version=4.0.14-unstable
 minecraft_version=20w11a
 yarn_version=20w11a+build.6
 fabricloader_version=0.7.8+build.187

+ 16 - 6
src/main/java/me/shedaniel/rei/api/widgets/Arrow.java

@@ -24,12 +24,19 @@
 package me.shedaniel.rei.api.widgets;
 
 import me.shedaniel.rei.gui.widget.WidgetWithBounds;
+import org.jetbrains.annotations.NotNull;
 
 public abstract class Arrow extends WidgetWithBounds {
+    /**
+     * @return the x coordinate for the top left corner of this widget.
+     */
     public final int getX() {
         return getBounds().getX();
     }
     
+    /**
+     * @return the y coordinate for the top left corner of this widget.
+     */
     public final int getY() {
         return getBounds().getY();
     }
@@ -42,16 +49,17 @@ public abstract class Arrow extends WidgetWithBounds {
     /**
      * Sets the animation duration in milliseconds.
      *
-     * @param animationDurationMS animation duration in milliseconds, animation is disabled when below or equals to 0
+     * @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
+     * @param animationDurationMS animation duration in milliseconds, animation is disabled when below or equals to 0.
+     * @return the arrow itself.
      */
+    @NotNull
     public final Arrow animationDurationMS(double animationDurationMS) {
         setAnimationDuration(animationDurationMS);
         return this;
@@ -60,9 +68,10 @@ public abstract class Arrow extends WidgetWithBounds {
     /**
      * 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
+     * @param animationDurationTicks animation duration in ticks, animation is disabled when below or equals to 0.
+     * @return the arrow itself.
      */
+    @NotNull
     public final Arrow animationDurationTicks(double animationDurationTicks) {
         return animationDurationMS(animationDurationTicks * 50);
     }
@@ -70,8 +79,9 @@ public abstract class Arrow extends WidgetWithBounds {
     /**
      * Disables the animation.
      *
-     * @return the arrow itself
+     * @return the arrow itself.
      */
+    @NotNull
     public final Arrow disableAnimation() {
         return animationDurationMS(-1);
     }

+ 30 - 0
src/main/java/me/shedaniel/rei/api/widgets/BaseWidget.java

@@ -0,0 +1,30 @@
+package me.shedaniel.rei.api.widgets;
+
+import me.shedaniel.math.Point;
+import me.shedaniel.rei.gui.widget.WidgetWithBounds;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.function.BiPredicate;
+
+public abstract class BaseWidget<T extends BaseWidget<T>> extends WidgetWithBounds {
+    @Nullable
+    private BiPredicate<T, Point> containsMousePredicate;
+    
+    public final void setContainsMousePredicate(@Nullable BiPredicate<T, Point> predicate) {
+        this.containsMousePredicate = predicate;
+    }
+    
+    @NotNull
+    public final T containsMousePredicate(@Nullable BiPredicate<T, Point> predicate) {
+        setContainsMousePredicate(predicate);
+        return (T) this;
+    }
+    
+    @Override
+    public boolean containsMouse(double mouseX, double mouseY) {
+        if (containsMousePredicate != null)
+            return containsMousePredicate.test((T) this, new Point(mouseX, mouseY));
+        return super.containsMouse(mouseX, mouseY);
+    }
+}

+ 16 - 6
src/main/java/me/shedaniel/rei/api/widgets/BurningFire.java

@@ -24,12 +24,19 @@
 package me.shedaniel.rei.api.widgets;
 
 import me.shedaniel.rei.gui.widget.WidgetWithBounds;
+import org.jetbrains.annotations.NotNull;
 
 public abstract class BurningFire extends WidgetWithBounds {
+    /**
+     * @return the x coordinate for the top left corner of this widget.
+     */
     public final int getX() {
         return getBounds().getX();
     }
     
+    /**
+     * @return the y coordinate for the top left corner of this widget.
+     */
     public final int getY() {
         return getBounds().getY();
     }
@@ -42,16 +49,17 @@ public abstract class BurningFire extends WidgetWithBounds {
     /**
      * Sets the animation duration in milliseconds.
      *
-     * @param animationDurationMS animation duration in milliseconds, animation is disabled when below or equals to 0
+     * @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
+     * @param animationDurationMS animation duration in milliseconds, animation is disabled when below or equals to 0.
+     * @return the arrow itself.
      */
+    @NotNull
     public final BurningFire animationDurationMS(double animationDurationMS) {
         setAnimationDuration(animationDurationMS);
         return this;
@@ -60,9 +68,10 @@ public abstract class BurningFire extends WidgetWithBounds {
     /**
      * 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
+     * @param animationDurationTicks animation duration in ticks, animation is disabled when below or equals to 0.
+     * @return the arrow itself.
      */
+    @NotNull
     public final BurningFire animationDurationTicks(double animationDurationTicks) {
         return animationDurationMS(animationDurationTicks * 50);
     }
@@ -70,8 +79,9 @@ public abstract class BurningFire extends WidgetWithBounds {
     /**
      * Disables the animation.
      *
-     * @return the arrow itself
+     * @return the arrow itself.
      */
+    @NotNull
     public final BurningFire disableAnimation() {
         return animationDurationMS(-1);
     }

+ 160 - 0
src/main/java/me/shedaniel/rei/api/widgets/Button.java

@@ -0,0 +1,160 @@
+package me.shedaniel.rei.api.widgets;
+
+import me.shedaniel.math.api.Point;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.OptionalInt;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+public abstract class Button extends BaseWidget<Button> {
+    public abstract void setTextColor(@Nullable BiFunction<@NotNull Button, @NotNull Point, @NotNull Integer> textColorFunction);
+    
+    public final Button textColor(@Nullable BiFunction<@NotNull Button, @NotNull Point, @NotNull Integer> textColorFunction) {
+        setTextColor(textColorFunction);
+        return this;
+    }
+    
+    public abstract int getTextColor(Point mouse);
+    
+    public abstract void setTextureId(@Nullable BiFunction<@NotNull Button, @NotNull Point, @NotNull Integer> textureIdFunction);
+    
+    public final Button textureId(@Nullable BiFunction<@NotNull Button, @NotNull Point, @NotNull Integer> textureIdFunction) {
+        setTextureId(textureIdFunction);
+        return this;
+    }
+    
+    public abstract int getTextureId(Point mouse);
+    
+    public abstract void onClick();
+    
+    public abstract boolean isEnabled();
+    
+    public abstract void setEnabled(boolean enabled);
+    
+    public final Button enabled(boolean enabled) {
+        setEnabled(enabled);
+        return this;
+    }
+    
+    public abstract OptionalInt getTint();
+    
+    public abstract void setTint(int tint);
+    
+    public abstract void removeTint();
+    
+    public final Button tint(@Nullable Integer tint) {
+        if (tint == null)
+            removeTint();
+        else setTint(tint);
+        return this;
+    }
+    
+    @NotNull
+    public abstract String getText();
+    
+    public abstract void setText(@NotNull String text);
+    
+    @NotNull
+    public final Button text(@NotNull String text) {
+        setText(text);
+        return this;
+    }
+    
+    @Nullable
+    public abstract Consumer<Button> getOnClick();
+    
+    public abstract void setOnClick(@Nullable Consumer<Button> onClick);
+    
+    @NotNull
+    public final Button onClick(@Nullable Consumer<Button> onClick) {
+        setOnClick(onClick);
+        return this;
+    }
+    
+    @Nullable
+    public abstract Consumer<Button> getOnRender();
+    
+    public abstract void setOnRender(@Nullable Consumer<Button> onRender);
+    
+    @NotNull
+    public final Button onRender(@Nullable Consumer<Button> onRender) {
+        setOnRender(onRender);
+        return this;
+    }
+    
+    /**
+     * @return whether the button is focusable by pressing tab, ignored if not clickable.
+     */
+    public abstract boolean isFocusable();
+    
+    /**
+     * Sets whether the button is focusable by pressing tab, ignored if not clickable.
+     *
+     * @param focusable whether the button is focusable by pressing tab, ignored if not clickable.
+     */
+    public abstract void setFocusable(boolean focusable);
+    
+    /**
+     * Sets whether the button is focusable by pressing tab, ignored if not clickable.
+     *
+     * @param focusable whether the label is focusable by pressing tab, ignored if not clickable.
+     * @return the button itself.
+     */
+    @NotNull
+    public final Button focusable(boolean focusable) {
+        setFocusable(focusable);
+        return this;
+    }
+    
+    /**
+     * @return the tooltip from the current tooltip function, null if no tooltip.
+     */
+    @Nullable
+    public abstract String getTooltip();
+    
+    /**
+     * Sets the tooltip function used to get the tooltip.
+     *
+     * @param tooltip the tooltip function used to get the tooltip.
+     */
+    public abstract void setTooltip(@Nullable Function<@NotNull Button, @Nullable String> tooltip);
+    
+    /**
+     * Sets the tooltip.
+     *
+     * @param tooltip the lines of tooltip.
+     * @return the button itself.
+     */
+    @NotNull
+    public final Button tooltipLines(@NotNull String... tooltip) {
+        return tooltipLine(String.join("\n", tooltip));
+    }
+    
+    /**
+     * Sets the tooltip.
+     *
+     * @param tooltip the line of tooltip.
+     * @return the button itself.
+     */
+    @NotNull
+    public final Button tooltipLine(@Nullable String tooltip) {
+        return tooltipSupplier(label -> tooltip);
+    }
+    
+    /**
+     * Sets the tooltip function.
+     *
+     * @param tooltip the tooltip function used to get the tooltip.
+     * @return the button itself.
+     */
+    @NotNull
+    public final Button tooltipSupplier(@Nullable Function<@NotNull Button, @Nullable String> tooltip) {
+        setTooltip(tooltip);
+        return this;
+    }
+    
+    public abstract boolean isFocused();
+}

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

@@ -37,76 +37,179 @@ public abstract class Label extends WidgetWithBounds {
     public static final int CENTER = 0;
     public static final int RIGHT_ALIGNED = 1;
     
+    /**
+     * @return whether the label is clickable, ignores if onClick is set.
+     */
     public abstract boolean isClickable();
     
+    /**
+     * Sets whether the label is clickable, ignores if onClick is set.
+     *
+     * @param clickable whether the label is clickable.
+     */
     public abstract void setClickable(boolean clickable);
     
+    /**
+     * Sets the label as clickable, ignores if onClick is set.
+     *
+     * @return the label itself.
+     */
+    @NotNull
     public final Label clickable() {
         return clickable(true);
     }
     
+    /**
+     * Sets whether the label is clickable, ignores if onClick is set.
+     *
+     * @param clickable whether the label is clickable.
+     * @return the label itself.
+     */
+    @NotNull
     public final Label clickable(boolean clickable) {
         setClickable(clickable);
         return this;
     }
     
+    /**
+     * @return the consumer on click, only applicable if the label is clickable, null if not set.
+     */
     @Nullable
     public abstract Consumer<Label> getOnClick();
     
+    /**
+     * Sets the on click consumer, only applicable if the label is clickable.
+     *
+     * @param onClick the on click consumer, only applicable if the label is clickable.
+     */
     public abstract void setOnClick(@Nullable Consumer<Label> onClick);
     
+    /**
+     * Sets the on click consumer, only applicable if the label is clickable.
+     *
+     * @param onClick the on click consumer, only applicable if the label is clickable.
+     * @return the label itself.
+     */
+    @NotNull
     public final Label onClick(@Nullable Consumer<Label> onClick) {
         setOnClick(onClick);
         return this;
     }
     
+    /**
+     * @return the consumer before render, null if not set.
+     */
     @Nullable
     public abstract Consumer<Label> getOnRender();
     
+    /**
+     * Sets the consumer before render.
+     *
+     * @param onRender the consumer before render.
+     */
     public abstract void setOnRender(@Nullable Consumer<Label> onRender);
     
+    /**
+     * Sets the consumer before render.
+     *
+     * @param onRender the consumer before render.
+     * @return the label itself.
+     */
+    @NotNull
     public final Label onRender(@Nullable Consumer<Label> onRender) {
         setOnRender(onRender);
         return this;
     }
     
+    /**
+     * @return whether the label is focusable by pressing tab, ignored if not clickable.
+     */
     public abstract boolean isFocusable();
     
+    /**
+     * Sets whether the label is focusable by pressing tab, ignored if not clickable.
+     *
+     * @param focusable whether the label is focusable by pressing tab, ignored if not clickable.
+     */
     public abstract void setFocusable(boolean focusable);
     
+    /**
+     * Sets whether the label is focusable by pressing tab, ignored if not clickable.
+     *
+     * @param focusable whether the label is focusable by pressing tab, ignored if not clickable.
+     * @return the label itself.
+     */
+    @NotNull
     public final Label focusable(boolean focusable) {
         setFocusable(focusable);
         return this;
     }
     
+    /**
+     * @return the tooltip from the current tooltip function, null if no tooltip.
+     */
     @Nullable
     public abstract String getTooltip();
     
+    /**
+     * Sets the tooltip function used to get the tooltip.
+     *
+     * @param tooltip the tooltip function used to get the tooltip.
+     */
     public abstract void setTooltip(@Nullable Function<Label, @Nullable String> tooltip);
     
+    /**
+     * Sets the tooltip.
+     *
+     * @param tooltip the lines of tooltip.
+     * @return the label itself.
+     */
+    @NotNull
     public final Label tooltipLines(@NotNull String... tooltip) {
         return tooltipLine(String.join("\n", tooltip));
     }
     
+    /**
+     * Sets the tooltip.
+     *
+     * @param tooltip the line of tooltip.
+     * @return the label itself.
+     */
+    @NotNull
     public final Label tooltipLine(@Nullable String tooltip) {
         return tooltipSupplier(label -> tooltip);
     }
     
+    /**
+     * Sets the tooltip function.
+     *
+     * @param tooltip the tooltip function used to get the tooltip.
+     * @return the label itself.
+     */
+    @NotNull
     public final Label tooltipSupplier(@Nullable Function<Label, @Nullable String> tooltip) {
         setTooltip(tooltip);
         return this;
     }
     
+    /**
+     * Gets the horizontal alignment of the label, defaulted as centered.
+     *
+     * @return {@link Label#LEFT_ALIGNED} if left aligned, {@link Label#CENTER} if centered or {@link Label#RIGHT_ALIGNED} if right aligned}.
+     */
     public abstract int getHorizontalAlignment();
     
+    @NotNull
     public final Label centered() {
         return horizontalAlignment(CENTER);
     }
     
+    @NotNull
     public final Label leftAligned() {
         return horizontalAlignment(LEFT_ALIGNED);
     }
     
+    @NotNull
     public final Label rightAligned() {
         return horizontalAlignment(RIGHT_ALIGNED);
     }
@@ -120,16 +223,19 @@ public abstract class Label extends WidgetWithBounds {
     
     public abstract boolean hasShadow();
     
+    @NotNull
     public final Label noShadow() {
         return shadow(false);
     }
     
+    @NotNull
     public final Label shadow() {
         return shadow(true);
     }
     
     public abstract void setShadow(boolean hasShadow);
     
+    @NotNull
     public final Label shadow(boolean hasShadow) {
         setShadow(hasShadow);
         return this;
@@ -139,10 +245,12 @@ public abstract class Label extends WidgetWithBounds {
     
     public abstract void setColor(int color);
     
+    @NotNull
     public final Label color(int lightModeColor, int darkModeColor) {
         return color(REIHelper.getInstance().isDarkThemeEnabled() ? darkModeColor : lightModeColor);
     }
     
+    @NotNull
     public final Label color(int color) {
         setColor(color);
         return this;
@@ -152,10 +260,12 @@ public abstract class Label extends WidgetWithBounds {
     
     public abstract void setHoveredColor(int hoveredColor);
     
+    @NotNull
     public final Label hoveredColor(int lightModeColor, int darkModeColor) {
         return hoveredColor(REIHelper.getInstance().isDarkThemeEnabled() ? darkModeColor : lightModeColor);
     }
     
+    @NotNull
     public final Label hoveredColor(int color) {
         setHoveredColor(color);
         return this;
@@ -174,6 +284,7 @@ public abstract class Label extends WidgetWithBounds {
     
     public abstract void setPoint(@NotNull Point point);
     
+    @NotNull
     public final Label point(@NotNull Point point) {
         setPoint(point);
         return this;
@@ -184,6 +295,7 @@ public abstract class Label extends WidgetWithBounds {
     
     public abstract void setText(@NotNull String text);
     
+    @NotNull
     public final Label text(@NotNull String text) {
         setText(text);
         return this;

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

@@ -34,11 +34,13 @@ public abstract class Panel extends WidgetWithBounds {
     
     public abstract void setInnerColor(int innerColor);
     
+    @NotNull
     public final Panel innerColor(int innerColor) {
         setInnerColor(innerColor);
         return this;
     }
     
+    @NotNull
     public final Panel innerColor(int lightColor, int darkColor) {
         return innerColor(REIHelper.getInstance().isDarkThemeEnabled() ? darkColor : lightColor);
     }
@@ -47,6 +49,7 @@ public abstract class Panel extends WidgetWithBounds {
     
     public abstract void setXTextureOffset(int xTextureOffset);
     
+    @NotNull
     public final Panel xTextureOffset(int xTextureOffset) {
         setXTextureOffset(xTextureOffset);
         return this;
@@ -56,6 +59,7 @@ public abstract class Panel extends WidgetWithBounds {
     
     public abstract void setYTextureOffset(int yTextureOffset);
     
+    @NotNull
     public final Panel yTextureOffset(int yTextureOffset) {
         setYTextureOffset(yTextureOffset);
         return this;
@@ -65,11 +69,13 @@ public abstract class Panel extends WidgetWithBounds {
     
     public abstract void setColor(int color);
     
+    @NotNull
     public final Panel color(int color) {
         setColor(color);
         return this;
     }
     
+    @NotNull
     public final Panel color(int lightColor, int darkColor) {
         return color(REIHelper.getInstance().isDarkThemeEnabled() ? darkColor : lightColor);
     }
@@ -79,6 +85,7 @@ public abstract class Panel extends WidgetWithBounds {
     
     public abstract void setRendering(@NotNull Predicate<Panel> rendering);
     
+    @NotNull
     public final Panel rendering(@NotNull Predicate<Panel> rendering) {
         setRendering(rendering);
         return this;

+ 30 - 2
src/main/java/me/shedaniel/rei/api/widgets/Slot.java

@@ -27,22 +27,26 @@ import me.shedaniel.math.Point;
 import me.shedaniel.rei.api.EntryStack;
 import me.shedaniel.rei.gui.widget.QueuedTooltip;
 import me.shedaniel.rei.gui.widget.WidgetWithBounds;
+import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import java.util.Collection;
 import java.util.List;
 
 public abstract class Slot extends WidgetWithBounds {
+    @NotNull
     public Slot unmarkInputOrOutput() {
         setNoticeMark((byte) 0);
         return this;
     }
     
+    @NotNull
     public final Slot markInput() {
         setNoticeMark((byte) 1);
         return this;
     }
     
+    @NotNull
     public final Slot markOutput() {
         setNoticeMark((byte) 2);
         return this;
@@ -56,12 +60,19 @@ public abstract class Slot extends WidgetWithBounds {
     
     public abstract boolean isInteractable();
     
+    @NotNull
     public Slot interactable(boolean interactable) {
         setInteractable(interactable);
         return this;
     }
     
-    public Slot noInteractable() {
+    @NotNull
+    public final Slot noInteractable() {
+        return interactable(false);
+    }
+    
+    @NotNull
+    public final Slot notInteractable() {
         return interactable(false);
     }
     
@@ -69,12 +80,19 @@ public abstract class Slot extends WidgetWithBounds {
     
     public abstract boolean isInteractableFavorites();
     
+    @NotNull
     public Slot interactableFavorites(boolean interactableFavorites) {
         setInteractableFavorites(interactableFavorites);
         return this;
     }
     
-    public Slot noFavoritesInteractable() {
+    @NotNull
+    public final Slot noFavoritesInteractable() {
+        return interactableFavorites(false);
+    }
+    
+    @NotNull
+    public final Slot notFavoritesInteractable() {
         return interactableFavorites(false);
     }
     
@@ -82,11 +100,13 @@ public abstract class Slot extends WidgetWithBounds {
     
     public abstract boolean isHighlightEnabled();
     
+    @NotNull
     public final Slot highlightEnabled(boolean highlight) {
         setHighlightEnabled(highlight);
         return this;
     }
     
+    @NotNull
     public final Slot disableHighlight() {
         return highlightEnabled(false);
     }
@@ -95,11 +115,13 @@ public abstract class Slot extends WidgetWithBounds {
     
     public abstract boolean isTooltipsEnabled();
     
+    @NotNull
     public final Slot tooltipsEnabled(boolean tooltipsEnabled) {
         setTooltipsEnabled(tooltipsEnabled);
         return this;
     }
     
+    @NotNull
     public final Slot disableTooltips() {
         return tooltipsEnabled(false);
     }
@@ -108,21 +130,27 @@ public abstract class Slot extends WidgetWithBounds {
     
     public abstract boolean isBackgroundEnabled();
     
+    @NotNull
     public final Slot backgroundEnabled(boolean backgroundEnabled) {
         setBackgroundEnabled(backgroundEnabled);
         return this;
     }
     
+    @NotNull
     public final Slot disableBackground() {
         return backgroundEnabled(false);
     }
     
+    @NotNull
     public abstract Slot clearEntries();
     
+    @NotNull
     public abstract Slot entry(EntryStack stack);
     
+    @NotNull
     public abstract Slot entries(Collection<EntryStack> stacks);
     
+    @NotNull
     public abstract List<EntryStack> getEntries();
     
     /**

+ 5 - 0
src/main/java/me/shedaniel/rei/api/widgets/Tooltip.java

@@ -26,23 +26,28 @@ package me.shedaniel.rei.api.widgets;
 import me.shedaniel.math.Point;
 import me.shedaniel.rei.api.REIHelper;
 import me.shedaniel.rei.gui.widget.QueuedTooltip;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.Collection;
 import java.util.List;
 
 public interface Tooltip {
+    @NotNull
     static Tooltip create(Point point, Collection<String> texts) {
         return QueuedTooltip.create(point, texts);
     }
     
+    @NotNull
     static Tooltip create(Point point, String... texts) {
         return QueuedTooltip.create(point, texts);
     }
     
+    @NotNull
     static Tooltip create(Collection<String> texts) {
         return QueuedTooltip.create(texts);
     }
     
+    @NotNull
     static Tooltip create(String... texts) {
         return QueuedTooltip.create(texts);
     }

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

@@ -32,12 +32,19 @@ 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.gui.Drawable;
+import net.minecraft.client.gui.DrawableHelper;
+import net.minecraft.client.gui.Element;
 import net.minecraft.client.sound.PositionedSoundInstance;
 import net.minecraft.sound.SoundEvents;
+import net.minecraft.text.Text;
 import net.minecraft.util.Identifier;
 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;
 
 public final class Widgets {
@@ -48,6 +55,32 @@ public final class Widgets {
         return new DrawableWidget(drawable);
     }
     
+    @NotNull
+    public static Widget wrapVanillaWidget(@NotNull Element element) {
+        return new VanillaWrappedWidget(element);
+    }
+    
+    private static class VanillaWrappedWidget extends Widget {
+        private Element element;
+        
+        public VanillaWrappedWidget(Element element) {
+            this.element = Objects.requireNonNull(element);
+        }
+        
+        @Override
+        public void render(int mouseX, int mouseY, float delta) {
+            if (element instanceof DrawableHelper)
+                ((DrawableHelper) element).setZOffset(getZ());
+            if (element instanceof Drawable)
+                ((Drawable) element).render(mouseX, mouseY, delta);
+        }
+        
+        @Override
+        public List<? extends Element> children() {
+            return Collections.singletonList(element);
+        }
+    }
+    
     @NotNull
     public static Widget createTexturedWidget(@NotNull Identifier identifier, @NotNull Rectangle bounds) {
         return createTexturedWidget(identifier, bounds, 0, 0);
@@ -167,6 +200,16 @@ public final class Widgets {
         return EntryWidget.create(point.x, point.y);
     }
     
+    @NotNull
+    public static Button createButton(@NotNull Rectangle bounds, @NotNull String text) {
+        return new ButtonWidget(bounds, text);
+    }
+    
+    @NotNull
+    public static Button createButton(@NotNull Rectangle bounds, @NotNull Text text) {
+        return new ButtonWidget(bounds, text);
+    }
+    
     public static void produceClickSound() {
         MinecraftClient.getInstance().getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
     }

+ 127 - 146
src/main/java/me/shedaniel/rei/gui/ContainerScreenOverlay.java

@@ -30,20 +30,24 @@ 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.Button;
 import me.shedaniel.rei.api.widgets.Tooltip;
 import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.config.SearchFieldLocation;
 import me.shedaniel.rei.gui.widget.*;
+import me.shedaniel.rei.impl.InternalWidgets;
 import me.shedaniel.rei.impl.ScreenHelper;
 import me.shedaniel.rei.impl.Weather;
 import me.shedaniel.rei.listeners.ContainerScreenHooks;
 import me.shedaniel.rei.utils.CollectionUtils;
+import net.minecraft.block.Blocks;
 import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.Element;
 import net.minecraft.client.gui.screen.Screen;
 import net.minecraft.client.gui.screen.ingame.HandledScreen;
 import net.minecraft.client.render.Tessellator;
 import net.minecraft.client.render.VertexConsumerProvider;
+import net.minecraft.client.render.item.ItemRenderer;
 import net.minecraft.client.resource.language.I18n;
 import net.minecraft.client.sound.PositionedSoundInstance;
 import net.minecraft.client.util.NarratorManager;
@@ -60,6 +64,7 @@ import net.minecraft.util.Identifier;
 import net.minecraft.world.GameMode;
 import org.apache.logging.log4j.util.TriConsumer;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import java.util.*;
@@ -105,9 +110,8 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
     };
     private Rectangle bounds;
     private Window window;
-    @Nullable private LateRenderedButton craftableToggleButton;
-    private LateRenderedButton configButton;
-    private ButtonWidget leftButton, rightButton;
+    private List<LateRenderable> lateRenderables = Lists.newArrayList();
+    private Button leftButton, rightButton;
     
     public static EntryListWidget getEntryListWidget() {
         return ENTRY_LIST_WIDGET;
@@ -144,131 +148,98 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
         this.widgets.add(ScreenHelper.getSearchField());
         ScreenHelper.getSearchField().setChangedListener(s -> ENTRY_LIST_WIDGET.updateSearch(s, false));
         if (!ConfigObject.getInstance().isEntryListWidgetScrolled()) {
-            widgets.add(leftButton = new ButtonWidget(new Rectangle(bounds.x, bounds.y + (ConfigObject.getInstance().getSearchFieldLocation() == SearchFieldLocation.TOP_SIDE ? 24 : 0) + 5, 16, 16), new TranslatableText("text.rei.left_arrow")) {
-                @Override
-                public void onPressed() {
-                    ENTRY_LIST_WIDGET.previousPage();
-                    if (ENTRY_LIST_WIDGET.getPage() < 0)
-                        ENTRY_LIST_WIDGET.setPage(ENTRY_LIST_WIDGET.getTotalPages() - 1);
-                    ENTRY_LIST_WIDGET.updateEntriesPosition();
-                }
-                
-                @Override
-                public boolean containsMouse(double mouseX, double mouseY) {
-                    return isNotInExclusionZones(mouseX, mouseY) && super.containsMouse(mouseX, mouseY);
-                }
-            }.tooltip(() -> I18n.translate("text.rei.previous_page")).canChangeFocuses(false));
-            widgets.add(rightButton = new ButtonWidget(new Rectangle(bounds.x + bounds.width - 18, bounds.y + (ConfigObject.getInstance().getSearchFieldLocation() == SearchFieldLocation.TOP_SIDE ? 24 : 0) + 5, 16, 16), new TranslatableText("text.rei.right_arrow")) {
-                @Override
-                public void onPressed() {
-                    ENTRY_LIST_WIDGET.nextPage();
-                    if (ENTRY_LIST_WIDGET.getPage() >= ENTRY_LIST_WIDGET.getTotalPages())
-                        ENTRY_LIST_WIDGET.setPage(0);
-                    ENTRY_LIST_WIDGET.updateEntriesPosition();
-                }
-                
-                @Override
-                public boolean containsMouse(double mouseX, double mouseY) {
-                    return isNotInExclusionZones(mouseX, mouseY) && super.containsMouse(mouseX, mouseY);
-                }
-            }.tooltip(() -> I18n.translate("text.rei.next_page")).canChangeFocuses(false));
+            widgets.add(leftButton = Widgets.createButton(new Rectangle(bounds.x, bounds.y + (ConfigObject.getInstance().getSearchFieldLocation() == SearchFieldLocation.TOP_SIDE ? 24 : 0) + 5, 16, 16), new TranslatableText("text.rei.left_arrow"))
+                    .onClick(button -> {
+                        ENTRY_LIST_WIDGET.previousPage();
+                        if (ENTRY_LIST_WIDGET.getPage() < 0)
+                            ENTRY_LIST_WIDGET.setPage(ENTRY_LIST_WIDGET.getTotalPages() - 1);
+                        ENTRY_LIST_WIDGET.updateEntriesPosition();
+                    })
+                    .containsMousePredicate((button, point) -> button.getBounds().contains(point) && isNotInExclusionZones(point.x, point.y))
+                    .tooltipLine(I18n.translate("text.rei.previous_page"))
+                    .focusable(false));
+            widgets.add(rightButton = Widgets.createButton(new Rectangle(bounds.x + bounds.width - 18, bounds.y + (ConfigObject.getInstance().getSearchFieldLocation() == SearchFieldLocation.TOP_SIDE ? 24 : 0) + 5, 16, 16), new TranslatableText("text.rei.right_arrow"))
+                    .onClick(button -> {
+                        ENTRY_LIST_WIDGET.nextPage();
+                        if (ENTRY_LIST_WIDGET.getPage() >= ENTRY_LIST_WIDGET.getTotalPages())
+                            ENTRY_LIST_WIDGET.setPage(0);
+                        ENTRY_LIST_WIDGET.updateEntriesPosition();
+                    })
+                    .containsMousePredicate((button, point) -> button.getBounds().contains(point) && isNotInExclusionZones(point.x, point.y))
+                    .tooltipLine(I18n.translate("text.rei.next_page"))
+                    .focusable(false));
         }
         
-        widgets.add(configButton = new LateRenderedButton(getConfigButtonArea(), NarratorManager.EMPTY) {
-            @Override
-            public void onPressed() {
-                if (Screen.hasShiftDown()) {
-                    ClientHelper.getInstance().setCheating(!ClientHelper.getInstance().isCheating());
-                    return;
-                }
-                ConfigManager.getInstance().openConfigScreen(ScreenHelper.getLastHandledScreen());
-            }
-            
-            @Override
-            public void render(int mouseX, int mouseY, float delta) {
-            }
-            
-            @Override
-            public void lateRender(int mouseX, int mouseY, float delta) {
-                setZOffset(600);
-                super.render(mouseX, mouseY, delta);
-                Rectangle bounds = getBounds();
-                if (ClientHelper.getInstance().isCheating() && RoughlyEnoughItemsCore.hasOperatorPermission()) {
-                    if (RoughlyEnoughItemsCore.hasPermissionToUsePackets())
-                        fillGradient(bounds.x + 1, bounds.y + 1, bounds.getMaxX() - 1, bounds.getMaxY() - 1, 721354752, 721354752);
-                    else
-                        fillGradient(bounds.x + 1, bounds.y + 1, bounds.getMaxX() - 1, bounds.getMaxY() - 1, 1476440063, 1476440063);
-                }
-                MinecraftClient.getInstance().getTextureManager().bindTexture(CHEST_GUI_TEXTURE);
-                RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
-                blit(bounds.x + 3, bounds.y + 3, 0, 0, 14, 14);
-                setZOffset(0);
-            }
-            
-            @Override
-            public Optional<String> getTooltips() {
-                String tooltips = I18n.translate("text.rei.config_tooltip");
-                tooltips += "\n  ";
-                if (!ClientHelper.getInstance().isCheating())
-                    tooltips += "\n" + I18n.translate("text.rei.cheating_disabled");
-                else if (!RoughlyEnoughItemsCore.hasOperatorPermission())
-                    tooltips += "\n" + I18n.translate("text.rei.cheating_enabled_no_perms");
-                else if (RoughlyEnoughItemsCore.hasPermissionToUsePackets())
-                    tooltips += "\n" + I18n.translate("text.rei.cheating_enabled");
-                else
-                    tooltips += "\n" + I18n.translate("text.rei.cheating_limited_enabled");
-                return Optional.ofNullable(tooltips);
-            }
-            
-            @Override
-            public boolean changeFocus(boolean boolean_1) {
-                return false;
-            }
-            
-            @Override
-            public boolean containsMouse(double mouseX, double mouseY) {
-                return isNotInExclusionZones(mouseX, mouseY) && super.containsMouse(mouseX, mouseY);
-            }
-        });
+        final Rectangle configButtonArea = getConfigButtonArea();
+        LateRenderable tmp;
+        widgets.add((Widget) (tmp = InternalWidgets.wrapLateRenderable(InternalWidgets.mergeWidgets(
+                Widgets.createButton(configButtonArea, NarratorManager.EMPTY)
+                        .onClick(button -> {
+                            if (Screen.hasShiftDown()) {
+                                ClientHelper.getInstance().setCheating(!ClientHelper.getInstance().isCheating());
+                                return;
+                            }
+                            ConfigManager.getInstance().openConfigScreen(ScreenHelper.getLastHandledScreen());
+                        })
+                        .onRender(button -> {
+                            if (ClientHelper.getInstance().isCheating() && RoughlyEnoughItemsCore.hasOperatorPermission()) {
+                                button.setTint(RoughlyEnoughItemsCore.hasPermissionToUsePackets() ? 721354752 : 1476440063);
+                            } else {
+                                button.removeTint();
+                            }
+                        })
+                        .focusable(false)
+                        .containsMousePredicate((button, point) -> button.getBounds().contains(point) && isNotInExclusionZones(point.x, point.y))
+                        .tooltipSupplier(button -> {
+                            String tooltips = I18n.translate("text.rei.config_tooltip");
+                            tooltips += "\n  ";
+                            if (!ClientHelper.getInstance().isCheating())
+                                tooltips += "\n" + I18n.translate("text.rei.cheating_disabled");
+                            else if (!RoughlyEnoughItemsCore.hasOperatorPermission())
+                                tooltips += "\n" + I18n.translate("text.rei.cheating_enabled_no_perms");
+                            else if (RoughlyEnoughItemsCore.hasPermissionToUsePackets())
+                                tooltips += "\n" + I18n.translate("text.rei.cheating_enabled");
+                            else
+                                tooltips += "\n" + I18n.translate("text.rei.cheating_limited_enabled");
+                            return tooltips;
+                        }),
+                Widgets.createDrawableWidget((helper, mouseX, mouseY, delta) -> {
+                    helper.setZOffset(helper.getZOffset() + 1);
+                    MinecraftClient.getInstance().getTextureManager().bindTexture(CHEST_GUI_TEXTURE);
+                    helper.blit(configButtonArea.x + 3, configButtonArea.y + 3, 0, 0, 14, 14);
+                })
+                )
+        )));
+        ((Widget) tmp).setZ(600);
+        lateRenderables.add(tmp);
+//        widgets.add((Widget) (tmp = InternalWidgets.wrapLateRenderable(Widgets.createTexturedWidget(CHEST_GUI_TEXTURE, configButtonArea.x + 3, configButtonArea.y + 3, 0, 0, 14, 14))));
+//        widgets.add((Widget) (tmp = InternalWidgets.wrapLateRenderable(Widgets.createDrawableWidget((helper, mouseX, mouseY, delta) -> {
+//            RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
+//            MinecraftClient.getInstance().getTextureManager().bindTexture(CHEST_GUI_TEXTURE);
+//            helper.blit(configButtonArea.x + 3, configButtonArea.y + 3, 0, 0, 14, 14);
+//        }))));
+//        ((Widget) tmp).setZ(600);
+//        lateRenderables.add(tmp);
         if (ConfigObject.getInstance().doesShowUtilsButtons()) {
-            widgets.add(new ButtonWidget(ConfigObject.getInstance().isLowerConfigButton() ? new Rectangle(ConfigObject.getInstance().isLeftHandSidePanel() ? window.getScaledWidth() - 30 : 10, 10, 20, 20) : new Rectangle(ConfigObject.getInstance().isLeftHandSidePanel() ? window.getScaledWidth() - 55 : 35, 10, 20, 20), NarratorManager.EMPTY) {
-                @Override
-                public void onPressed() {
-                    MinecraftClient.getInstance().player.sendChatMessage(ConfigObject.getInstance().getGamemodeCommand().replaceAll("\\{gamemode}", getNextGameMode(Screen.hasShiftDown()).getName()));
-                }
-                
-                @Override
-                public void render(int mouseX, int mouseY, float delta) {
-                    setText(getGameModeShortText(getCurrentGameMode()));
-                    super.render(mouseX, mouseY, delta);
-                }
-                
-                @Override
-                public boolean containsMouse(double mouseX, double mouseY) {
-                    return isNotInExclusionZones(mouseX, mouseY) && super.containsMouse(mouseX, mouseY);
-                }
-            }.tooltip(() -> I18n.translate("text.rei.gamemode_button.tooltip", getGameModeText(getNextGameMode(Screen.hasShiftDown())))).canChangeFocuses(false));
+            widgets.add(Widgets.createButton(ConfigObject.getInstance().isLowerConfigButton() ? new Rectangle(ConfigObject.getInstance().isLeftHandSidePanel() ? window.getScaledWidth() - 30 : 10, 10, 20, 20) : new Rectangle(ConfigObject.getInstance().isLeftHandSidePanel() ? window.getScaledWidth() - 55 : 35, 10, 20, 20), NarratorManager.EMPTY)
+                    .onClick(button -> MinecraftClient.getInstance().player.sendChatMessage(ConfigObject.getInstance().getGamemodeCommand().replaceAll("\\{gamemode}", getNextGameMode(Screen.hasShiftDown()).getName())))
+                    .onRender(button -> button.setText(getGameModeShortText(getCurrentGameMode())))
+                    .focusable(false)
+                    .tooltipLine(I18n.translate("text.rei.gamemode_button.tooltip", getGameModeText(getNextGameMode(Screen.hasShiftDown()))))
+                    .containsMousePredicate((button, point) -> button.getBounds().contains(point) && isNotInExclusionZones(point.x, point.y)));
             int xxx = ConfigObject.getInstance().isLeftHandSidePanel() ? window.getScaledWidth() - 30 : 10;
             for (Weather weather : Weather.values()) {
-                widgets.add(new ButtonWidget(new Rectangle(xxx, 35, 20, 20), NarratorManager.EMPTY) {
-                    @Override
-                    public void onPressed() {
-                        MinecraftClient.getInstance().player.sendChatMessage(ConfigObject.getInstance().getWeatherCommand().replaceAll("\\{weather}", weather.name().toLowerCase(Locale.ROOT)));
-                    }
-                    
-                    @Override
-                    public void render(int mouseX, int mouseY, float delta) {
-                        super.render(mouseX, mouseY, delta);
-                        MinecraftClient.getInstance().getTextureManager().bindTexture(CHEST_GUI_TEXTURE);
-                        RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
-                        blit(getBounds().x + 3, getBounds().y + 3, weather.getId() * 14, 14, 14, 14);
-                    }
-                    
-                    @Override
-                    public boolean containsMouse(double mouseX, double mouseY) {
-                        return isNotInExclusionZones(mouseX, mouseY) && super.containsMouse(mouseX, mouseY);
-                    }
-                }.tooltip(() -> I18n.translate("text.rei.weather_button.tooltip", I18n.translate(weather.getTranslateKey()))).canChangeFocuses(false));
+                Button weatherButton;
+                widgets.add(weatherButton = Widgets.createButton(new Rectangle(xxx, 35, 20, 20), NarratorManager.EMPTY)
+                        .onClick(button -> MinecraftClient.getInstance().player.sendChatMessage(ConfigObject.getInstance().getWeatherCommand().replaceAll("\\{weather}", weather.name().toLowerCase(Locale.ROOT))))
+                        .tooltipLine(I18n.translate("text.rei.weather_button.tooltip", I18n.translate(weather.getTranslateKey())))
+                        .focusable(false)
+                        .containsMousePredicate((button, point) -> button.getBounds().contains(point) && isNotInExclusionZones(point.x, point.y)));
+                widgets.add(Widgets.createDrawableWidget((helper, mouseX, mouseY, delta) -> {
+                    MinecraftClient.getInstance().getTextureManager().bindTexture(CHEST_GUI_TEXTURE);
+                    RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
+                    helper.blit(weatherButton.getBounds().x + 3, weatherButton.getBounds().y + 3, weather.getId() * 14, 14, 14, 14);
+                }));
                 xxx += ConfigObject.getInstance().isLeftHandSidePanel() ? -25 : 25;
             }
         }
@@ -282,20 +253,27 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
             }));
         }
         if (ConfigObject.getInstance().isCraftableFilterEnabled()) {
-            this.widgets.add(craftableToggleButton = new CraftableToggleButtonWidget(getCraftableToggleArea()) {
-                @Override
-                public void onPressed() {
-                    ConfigManager.getInstance().toggleCraftableOnly();
-                    ENTRY_LIST_WIDGET.updateSearch(ScreenHelper.getSearchField().getText(), true);
-                }
-                
-                @Override
-                public boolean containsMouse(double mouseX, double mouseY) {
-                    return isNotInExclusionZones(mouseX, mouseY) && super.containsMouse(mouseX, mouseY);
-                }
-            });
-        } else {
-            craftableToggleButton = null;
+            Rectangle area = getCraftableToggleArea();
+            ItemRenderer itemRenderer = MinecraftClient.getInstance().getItemRenderer();
+            ItemStack icon = new ItemStack(Blocks.CRAFTING_TABLE);
+            this.widgets.add((Widget) (tmp = InternalWidgets.wrapLateRenderable(InternalWidgets.mergeWidgets(
+                    Widgets.createButton(area, NarratorManager.EMPTY)
+                            .focusable(false)
+                            .onClick(button -> {
+                                ConfigManager.getInstance().toggleCraftableOnly();
+                                ENTRY_LIST_WIDGET.updateSearch(ScreenHelper.getSearchField().getText(), true);
+                            })
+                            .onRender(button -> button.setTint(ConfigManager.getInstance().isCraftableOnlyEnabled() ? 939579655 : 956235776))
+                            .containsMousePredicate((button, point) -> button.getBounds().contains(point) && isNotInExclusionZones(point.x, point.y))
+                            .tooltipSupplier(button -> I18n.translate(ConfigManager.getInstance().isCraftableOnlyEnabled() ? "text.rei.showing_craftable" : "text.rei.showing_all")),
+                    Widgets.createDrawableWidget((helper, mouseX, mouseY, delta) -> {
+                        itemRenderer.zOffset = helper.getZOffset();
+                        itemRenderer.renderGuiItemIcon(icon, area.x + 2, area.y + 2);
+                        itemRenderer.zOffset = 0.0F;
+                    }))
+            )));
+            ((Widget) tmp).setZ(600);
+            lateRenderables.add(tmp);
         }
     }
     
@@ -387,6 +365,7 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
         return I18n.translate(String.format("%s%s", "text.rei.", ClientHelper.getInstance().isCheating() ? "cheat" : "nocheat"));
     }
     
+    @NotNull
     @Override
     public Rectangle getBounds() {
         return bounds;
@@ -438,9 +417,9 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
     public void lateRender(int mouseX, int mouseY, float delta) {
         if (ScreenHelper.isOverlayVisible()) {
             ScreenHelper.getSearchField().laterRender(mouseX, mouseY, delta);
-            if (craftableToggleButton != null)
-                craftableToggleButton.lateRender(mouseX, mouseY, delta);
-            configButton.lateRender(mouseX, mouseY, delta);
+            for (LateRenderable lateRenderable : lateRenderables) {
+                lateRenderable.lateRender(mouseX, mouseY, delta);
+            }
         }
         Screen currentScreen = MinecraftClient.getInstance().currentScreen;
         if (!(currentScreen instanceof RecipeViewingScreen) || !((RecipeViewingScreen) currentScreen).choosePageActivated)
@@ -478,8 +457,10 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
     public void renderWidgets(int int_1, int int_2, float float_1) {
         if (!ScreenHelper.isOverlayVisible())
             return;
-        if (!ConfigObject.getInstance().isEntryListWidgetScrolled())
-            leftButton.enabled = rightButton.enabled = ENTRY_LIST_WIDGET.getTotalPages() > 1;
+        if (!ConfigObject.getInstance().isEntryListWidgetScrolled()) {
+            leftButton.setEnabled(ENTRY_LIST_WIDGET.getTotalPages() > 1);
+            rightButton.setEnabled(ENTRY_LIST_WIDGET.getTotalPages() > 1);
+        }
         for (Widget widget : widgets) {
             widget.render(int_1, int_2, float_1);
         }
@@ -491,10 +472,10 @@ public class ContainerScreenOverlay extends WidgetWithBounds {
             return false;
         if (isInside(PointHelper.ofMouse())) {
             if (!ConfigObject.getInstance().isEntryListWidgetScrolled()) {
-                if (amount > 0 && leftButton.enabled)
-                    leftButton.onPressed();
-                else if (amount < 0 && rightButton.enabled)
-                    rightButton.onPressed();
+                if (amount > 0 && leftButton.isEnabled())
+                    leftButton.onClick();
+                else if (amount < 0 && rightButton.isEnabled())
+                    rightButton.onClick();
                 else
                     return false;
                 return true;

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

@@ -31,7 +31,6 @@ 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.Widget;
 import me.shedaniel.rei.gui.widget.WidgetWithBounds;
 import me.shedaniel.rei.impl.ScreenHelper;
@@ -46,6 +45,7 @@ import net.minecraft.util.Formatting;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.math.MathHelper;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.Collections;
 import java.util.List;
@@ -103,19 +103,12 @@ public class PreRecipeViewingScreen extends Screen {
     protected void init() {
         this.children.clear();
         this.widgets.clear();
-        this.widgets.add(new ButtonWidget(new Rectangle(width / 2 - 100, height - 40, 200, 20), NarratorManager.EMPTY) {
-            @Override
-            public void render(int mouseX, int mouseY, float delta) {
-                enabled = isSet;
-                setText(enabled ? I18n.translate("text.rei.select") : I18n.translate("config.roughlyenoughitems.recipeScreenType.unset"));
-                super.render(mouseX, mouseY, delta);
-            }
-            
-            @Override
-            public void onPressed() {
-                callback.accept(original);
-            }
-        });
+        this.widgets.add(Widgets.createButton(new Rectangle(width / 2 - 100, height - 40, 200, 20), NarratorManager.EMPTY)
+                .onRender(button -> {
+                    button.setEnabled(isSet);
+                    button.setText(isSet ? I18n.translate("text.rei.select") : I18n.translate("config.roughlyenoughitems.recipeScreenType.unset"));
+                })
+                .onClick(button -> callback.accept(original)));
         this.widgets.add(new ScreenTypeSelection(width / 2 - 200 - 5, height / 2 - 112 / 2 - 10, 0));
         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));
@@ -187,6 +180,7 @@ public class PreRecipeViewingScreen extends Screen {
             this.v = v;
         }
         
+        @NotNull
         @Override
         public Rectangle getBounds() {
             return bounds;

+ 72 - 60
src/main/java/me/shedaniel/rei/gui/RecipeViewingScreen.java

@@ -31,10 +31,15 @@ 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.Button;
 import me.shedaniel.rei.api.widgets.Panel;
 import me.shedaniel.rei.api.widgets.Widgets;
-import me.shedaniel.rei.gui.widget.*;
+import me.shedaniel.rei.gui.widget.EntryWidget;
+import me.shedaniel.rei.gui.widget.RecipeChoosePageWidget;
+import me.shedaniel.rei.gui.widget.TabWidget;
+import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.impl.ClientHelperImpl;
+import me.shedaniel.rei.impl.InternalWidgets;
 import me.shedaniel.rei.impl.ScreenHelper;
 import me.shedaniel.rei.impl.widgets.PanelWidget;
 import me.shedaniel.rei.utils.CollectionUtils;
@@ -82,7 +87,7 @@ public class RecipeViewingScreen extends Screen implements RecipeScreen {
     private Rectangle bounds;
     @Nullable
     private Panel workingStationsBaseWidget;
-    private ButtonWidget recipeBack, recipeNext, categoryBack, categoryNext;
+    private Button recipeBack, recipeNext, categoryBack, categoryNext;
     private EntryStack ingredientStackToNotice = EntryStack.empty();
     private EntryStack resultStackToNotice = EntryStack.empty();
     
@@ -174,13 +179,13 @@ public class RecipeViewingScreen extends Screen implements RecipeScreen {
         if (choosePageActivated)
             return recipeChoosePageWidget.keyPressed(int_1, int_2, int_3);
         else if (ConfigObject.getInstance().getNextPageKeybind().matchesKey(int_1, int_2)) {
-            if (recipeNext.enabled)
-                recipeNext.onPressed();
-            return recipeNext.enabled;
+            if (recipeNext.isEnabled())
+                recipeNext.onClick();
+            return recipeNext.isEnabled();
         } else if (ConfigObject.getInstance().getPreviousPageKeybind().matchesKey(int_1, int_2)) {
-            if (recipeBack.enabled)
-                recipeBack.onPressed();
-            return recipeBack.enabled;
+            if (recipeBack.isEnabled())
+                recipeBack.onClick();
+            return recipeBack.isEnabled();
         }
         for (Element element : children())
             if (element.keyPressed(int_1, int_2, int_3))
@@ -226,46 +231,51 @@ public class RecipeViewingScreen extends Screen implements RecipeScreen {
         }
         this.bounds = new Rectangle(width / 2 - guiWidth / 2, height / 2 - guiHeight / 2, guiWidth, guiHeight);
         this.page = MathHelper.clamp(page, 0, getTotalPages(selectedCategory) - 1);
-        ButtonWidget w, w2;
-        this.widgets.add(w = ButtonWidget.create(new Rectangle(bounds.x + 2, bounds.y - 16, 10, 10), new TranslatableText("text.rei.left_arrow"), buttonWidget -> {
-            categoryPages--;
-            if (categoryPages < 0)
-                categoryPages = MathHelper.ceil(categories.size() / (float) tabsPerPage) - 1;
-            RecipeViewingScreen.this.init();
-        }));
-        this.widgets.add(w2 = ButtonWidget.create(new Rectangle(bounds.x + bounds.width - 12, bounds.y - 16, 10, 10), new TranslatableText("text.rei.right_arrow"), buttonWidget -> {
-            categoryPages++;
-            if (categoryPages > MathHelper.ceil(categories.size() / (float) tabsPerPage) - 1)
-                categoryPages = 0;
-            RecipeViewingScreen.this.init();
-        }));
-        w.enabled = w2.enabled = categories.size() > tabsPerPage;
-        widgets.add(categoryBack = ButtonWidget.create(new Rectangle(bounds.getX() + 5, bounds.getY() + 5, 12, 12), new TranslatableText("text.rei.left_arrow"), buttonWidget -> {
-            int currentCategoryIndex = categories.indexOf(selectedCategory);
-            currentCategoryIndex--;
-            if (currentCategoryIndex < 0)
-                currentCategoryIndex = categories.size() - 1;
-            ClientHelperImpl.getInstance().openRecipeViewingScreen(categoriesMap, categories.get(currentCategoryIndex).getIdentifier(), ingredientStackToNotice, resultStackToNotice);
-        }).tooltip(() -> I18n.translate("text.rei.previous_category")));
+        this.widgets.add(Widgets.createButton(new Rectangle(bounds.x + 2, bounds.y - 16, 10, 10), new TranslatableText("text.rei.left_arrow"))
+                .onClick(button -> {
+                    categoryPages--;
+                    if (categoryPages < 0)
+                        categoryPages = MathHelper.ceil(categories.size() / (float) tabsPerPage) - 1;
+                    RecipeViewingScreen.this.init();
+                })
+                .enabled(categories.size() > tabsPerPage));
+        this.widgets.add(Widgets.createButton(new Rectangle(bounds.x + bounds.width - 12, bounds.y - 16, 10, 10), new TranslatableText("text.rei.right_arrow"))
+                .onClick(button -> {
+                    categoryPages++;
+                    if (categoryPages > MathHelper.ceil(categories.size() / (float) tabsPerPage) - 1)
+                        categoryPages = 0;
+                    RecipeViewingScreen.this.init();
+                })
+                .enabled(categories.size() > tabsPerPage));
+        widgets.add(categoryBack = Widgets.createButton(new Rectangle(bounds.getX() + 5, bounds.getY() + 5, 12, 12), new TranslatableText("text.rei.left_arrow"))
+                .onClick(button -> {
+                    int currentCategoryIndex = categories.indexOf(selectedCategory);
+                    currentCategoryIndex--;
+                    if (currentCategoryIndex < 0)
+                        currentCategoryIndex = categories.size() - 1;
+                    ClientHelperImpl.getInstance().openRecipeViewingScreen(categoriesMap, categories.get(currentCategoryIndex).getIdentifier(), ingredientStackToNotice, resultStackToNotice);
+                }).tooltipLine(I18n.translate("text.rei.previous_category")));
         widgets.add(Widgets.createClickableLabel(new Point(bounds.getCenterX(), bounds.getY() + 7), selectedCategory.getCategoryName(), clickableLabelWidget -> {
             ClientHelper.getInstance().executeViewAllRecipesKeyBind();
         }).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++;
-            if (currentCategoryIndex >= categories.size())
-                currentCategoryIndex = 0;
-            ClientHelperImpl.getInstance().openRecipeViewingScreen(categoriesMap, categories.get(currentCategoryIndex).getIdentifier(), ingredientStackToNotice, resultStackToNotice);
-        }).tooltip(() -> I18n.translate("text.rei.next_category")));
-        categoryBack.enabled = categories.size() > 1;
-        categoryNext.enabled = categories.size() > 1;
+        widgets.add(categoryNext = Widgets.createButton(new Rectangle(bounds.getMaxX() - 17, bounds.getY() + 5, 12, 12), new TranslatableText("text.rei.right_arrow"))
+                .onClick(button -> {
+                    int currentCategoryIndex = categories.indexOf(selectedCategory);
+                    currentCategoryIndex++;
+                    if (currentCategoryIndex >= categories.size())
+                        currentCategoryIndex = 0;
+                    ClientHelperImpl.getInstance().openRecipeViewingScreen(categoriesMap, categories.get(currentCategoryIndex).getIdentifier(), ingredientStackToNotice, resultStackToNotice);
+                }).tooltipLine(I18n.translate("text.rei.next_category")));
+        categoryBack.setEnabled(categories.size() > 1);
+        categoryNext.setEnabled(categories.size() > 1);
         
-        widgets.add(recipeBack = ButtonWidget.create(new Rectangle(bounds.getX() + 5, bounds.getY() + 19, 12, 12), new TranslatableText("text.rei.left_arrow"), buttonWidget -> {
-            page--;
-            if (page < 0)
-                page = getTotalPages(selectedCategory) - 1;
-            RecipeViewingScreen.this.init();
-        }).tooltip(() -> I18n.translate("text.rei.previous_page")));
+        widgets.add(recipeBack = Widgets.createButton(new Rectangle(bounds.getX() + 5, bounds.getY() + 19, 12, 12), new TranslatableText("text.rei.left_arrow"))
+                .onClick(button -> {
+                    page--;
+                    if (page < 0)
+                        page = getTotalPages(selectedCategory) - 1;
+                    RecipeViewingScreen.this.init();
+                }).tooltipLine(I18n.translate("text.rei.previous_page")));
         widgets.add(Widgets.createClickableLabel(new Point(bounds.getCenterX(), bounds.getY() + 21), "", label -> {
             RecipeViewingScreen.this.choosePageActivated = true;
             RecipeViewingScreen.this.init();
@@ -273,13 +283,15 @@ public class RecipeViewingScreen extends Screen implements RecipeScreen {
             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))
-                page = 0;
-            RecipeViewingScreen.this.init();
-        }).tooltip(() -> I18n.translate("text.rei.next_page")));
-        recipeBack.enabled = recipeNext.enabled = categoriesMap.get(selectedCategory).size() > getRecipesPerPageByHeight();
+        widgets.add(recipeNext = Widgets.createButton(new Rectangle(bounds.getMaxX() - 17, bounds.getY() + 19, 12, 12), new TranslatableText("text.rei.right_arrow"))
+                .onClick(button -> {
+                    page++;
+                    if (page >= getTotalPages(selectedCategory))
+                        page = 0;
+                    RecipeViewingScreen.this.init();
+                }).tooltipLine(I18n.translate("text.rei.next_page")));
+        recipeBack.setEnabled(categoriesMap.get(selectedCategory).size() > getRecipesPerPageByHeight());
+        recipeNext.setEnabled(categoriesMap.get(selectedCategory).size() > getRecipesPerPageByHeight());
         int tabV = isCompactTabs ? 166 : 192;
         for (int i = 0; i < tabsPerPage; i++) {
             int j = i + categoryPages * tabsPerPage;
@@ -309,7 +321,7 @@ public class RecipeViewingScreen extends Screen implements RecipeScreen {
             recipeBounds.put(displayBounds, setupDisplay);
             this.widgets.addAll(setupDisplay);
             if (supplier.isPresent() && supplier.get().get(displayBounds) != null)
-                this.widgets.add(new AutoCraftingButtonWidget(displayBounds, supplier.get().get(displayBounds), supplier.get().getButtonText(), displaySupplier, setupDisplay, selectedCategory));
+                this.widgets.add(InternalWidgets.createAutoCraftingButtonWidget(displayBounds, supplier.get().get(displayBounds), supplier.get().getButtonText(), displaySupplier, setupDisplay, selectedCategory));
         }
         if (choosePageActivated)
             recipeChoosePageWidget = new RecipeChoosePageWidget(this, page, getTotalPages(selectedCategory));
@@ -499,16 +511,16 @@ public class RecipeViewingScreen extends Screen implements RecipeScreen {
             if (listener.mouseScrolled(i, j, amount))
                 return true;
         if (getBounds().contains(PointHelper.ofMouse())) {
-            if (amount > 0 && recipeBack.enabled)
-                recipeBack.onPressed();
-            else if (amount < 0 && recipeNext.enabled)
-                recipeNext.onPressed();
+            if (amount > 0 && recipeBack.isEnabled())
+                recipeBack.onClick();
+            else if (amount < 0 && recipeNext.isEnabled())
+                recipeNext.onClick();
         }
         if ((new Rectangle(bounds.x, bounds.y - 28, bounds.width, 28)).contains(PointHelper.ofMouse())) {
-            if (amount > 0 && categoryBack.enabled)
-                categoryBack.onPressed();
-            else if (amount < 0 && categoryNext.enabled)
-                categoryNext.onPressed();
+            if (amount > 0 && categoryBack.isEnabled())
+                categoryBack.onClick();
+            else if (amount < 0 && categoryNext.isEnabled())
+                categoryNext.onClick();
         }
         return super.mouseScrolled(i, j, amount);
     }

+ 44 - 60
src/main/java/me/shedaniel/rei/gui/VillagerRecipeViewingScreen.java

@@ -31,13 +31,14 @@ 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.Button;
+import me.shedaniel.rei.api.widgets.Tooltip;
 import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.entries.RecipeEntry;
-import me.shedaniel.rei.gui.widget.AutoCraftingButtonWidget;
-import me.shedaniel.rei.gui.widget.ButtonWidget;
 import me.shedaniel.rei.gui.widget.TabWidget;
 import me.shedaniel.rei.gui.widget.Widget;
 import me.shedaniel.rei.impl.ClientHelperImpl;
+import me.shedaniel.rei.impl.InternalWidgets;
 import me.shedaniel.rei.impl.ScreenHelper;
 import me.shedaniel.rei.utils.CollectionUtils;
 import net.minecraft.client.MinecraftClient;
@@ -68,7 +69,7 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
     private final Map<RecipeCategory<?>, List<RecipeDisplay>> categoryMap;
     private final List<RecipeCategory<?>> categories;
     private final List<Widget> widgets = Lists.newArrayList();
-    private final List<ButtonWidget> buttonWidgets = Lists.newArrayList();
+    private final List<Button> buttonList = Lists.newArrayList();
     private final List<RecipeEntry> recipeRenderers = Lists.newArrayList();
     private final List<TabWidget> tabs = Lists.newArrayList();
     public Rectangle bounds, scrollListBounds;
@@ -134,7 +135,7 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
         this.draggingScrollBar = false;
         this.children.clear();
         this.widgets.clear();
-        this.buttonWidgets.clear();
+        this.buttonList.clear();
         this.recipeRenderers.clear();
         this.tabs.clear();
         int largestWidth = width - 100;
@@ -183,41 +184,22 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
         this.widgets.addAll(setupDisplay);
         Optional<ButtonAreaSupplier> supplier = RecipeHelper.getInstance().getAutoCraftButtonArea(category);
         if (supplier.isPresent() && supplier.get().get(recipeBounds) != null)
-            this.widgets.add(new AutoCraftingButtonWidget(recipeBounds, supplier.get().get(recipeBounds), supplier.get().getButtonText(), () -> display, setupDisplay, category));
+            this.widgets.add(InternalWidgets.createAutoCraftingButtonWidget(recipeBounds, supplier.get().get(recipeBounds), supplier.get().getButtonText(), () -> display, setupDisplay, category));
         
         int index = 0;
         for (RecipeDisplay recipeDisplay : categoryMap.get(category)) {
             int finalIndex = index;
             RecipeEntry recipeEntry;
             recipeRenderers.add(recipeEntry = category.getSimpleRenderer(recipeDisplay));
-            buttonWidgets.add(new ButtonWidget(new Rectangle(bounds.x + 5, 0, recipeEntry.getWidth(), recipeEntry.getHeight()), NarratorManager.EMPTY) {
-                @Override
-                public void onPressed() {
-                    selectedRecipeIndex = finalIndex;
-                    VillagerRecipeViewingScreen.this.init();
-                }
-                
-                @Override
-                public boolean isHovered(int mouseX, int mouseY) {
-                    return (isMouseOver(mouseX, mouseY) && scrollListBounds.contains(mouseX, mouseY)) || focused;
-                }
-                
-                @Override
-                protected int getTextureId(boolean boolean_1) {
-                    enabled = selectedRecipeIndex != finalIndex;
-                    return super.getTextureId(boolean_1);
-                }
-                
-                @Override
-                public boolean mouseClicked(double mouseX, double mouseY, int button) {
-                    if ((isMouseOver(mouseX, mouseY) && scrollListBounds.contains(mouseX, mouseY)) && enabled && button == 0) {
-                        minecraft.getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
-                        onPressed();
-                        return true;
-                    }
-                    return false;
-                }
-            });
+            buttonList.add(Widgets.createButton(new Rectangle(bounds.x + 5, 0, recipeEntry.getWidth(), recipeEntry.getHeight()), NarratorManager.EMPTY)
+                    .onClick(button -> {
+                        selectedRecipeIndex = finalIndex;
+                        VillagerRecipeViewingScreen.this.init();
+                    })
+                    .containsMousePredicate((button, point) -> {
+                        return (button.getBounds().contains(point) && scrollListBounds.contains(point)) || button.isFocused();
+                    })
+                    .onRender(button -> button.setEnabled(selectedRecipeIndex != finalIndex)));
             index++;
         }
         int tabV = isCompactTabs ? 166 : 192;
@@ -236,26 +218,28 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
                 tab.setRenderer(tabCategory, tabCategory.getLogo(), tabCategory.getCategoryName(), j == selectedCategoryIndex);
             }
         }
-        ButtonWidget w, w2;
-        this.widgets.add(w = ButtonWidget.create(new Rectangle(bounds.x + 2, bounds.y - 16, 10, 10), new TranslatableText("text.rei.left_arrow"), buttonWidget -> {
-            tabsPage--;
-            if (tabsPage < 0)
-                tabsPage = MathHelper.ceil(categories.size() / (float) tabsPerPage) - 1;
-            VillagerRecipeViewingScreen.this.init();
-        }));
-        this.widgets.add(w2 = ButtonWidget.create(new Rectangle(bounds.x + bounds.width - 12, bounds.y - 16, 10, 10), new TranslatableText("text.rei.right_arrow"), buttonWidget -> {
-            tabsPage++;
-            if (tabsPage > MathHelper.ceil(categories.size() / (float) tabsPerPage) - 1)
-                tabsPage = 0;
-            VillagerRecipeViewingScreen.this.init();
-        }));
-        w.enabled = w2.enabled = categories.size() > tabsPerPage;
+        this.widgets.add(Widgets.createButton(new Rectangle(bounds.x + 2, bounds.y - 16, 10, 10), new TranslatableText("text.rei.left_arrow"))
+                .onClick(button -> {
+                    tabsPage--;
+                    if (tabsPage < 0)
+                        tabsPage = MathHelper.ceil(categories.size() / (float) tabsPerPage) - 1;
+                    VillagerRecipeViewingScreen.this.init();
+                })
+                .enabled(categories.size() > tabsPerPage));
+        this.widgets.add(Widgets.createButton(new Rectangle(bounds.x + bounds.width - 12, bounds.y - 16, 10, 10), new TranslatableText("text.rei.right_arrow"))
+                .onClick(button -> {
+                    tabsPage++;
+                    if (tabsPage > MathHelper.ceil(categories.size() / (float) tabsPerPage) - 1)
+                        tabsPage = 0;
+                    VillagerRecipeViewingScreen.this.init();
+                })
+                .enabled(categories.size() > tabsPerPage));
         
         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.children.addAll(buttonList);
         this.widgets.addAll(tabs);
         this.children.addAll(widgets);
         this.children.add(ScreenHelper.getLastOverlay(true, false));
@@ -310,7 +294,7 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
     
     @Override
     public boolean mouseScrolled(double double_1, double double_2, double double_3) {
-        double height = CollectionUtils.sumInt(buttonWidgets, b -> b.getBounds().getHeight());
+        double height = CollectionUtils.sumInt(buttonList, b -> b.getBounds().getHeight());
         if (scrollListBounds.contains(double_1, double_2) && height > scrollListBounds.height - 2) {
             offset(ClothConfigInitializer.getScrollStep() * -double_3, true);
             if (scrollBarAlphaFuture == 0)
@@ -340,7 +324,7 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
     }
     
     private double getMaxScrollPosition() {
-        return CollectionUtils.sumInt(buttonWidgets, b -> b.getBounds().getHeight());
+        return CollectionUtils.sumInt(buttonList, b -> b.getBounds().getHeight());
     }
     
     @Override
@@ -376,18 +360,18 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
         ScreenHelper.getLastOverlay().render(mouseX, mouseY, delta);
         RenderSystem.pushMatrix();
         ScissorsHandler.INSTANCE.scissor(new Rectangle(0, scrollListBounds.y + 1, width, scrollListBounds.height - 2));
-        for (ButtonWidget buttonWidget : buttonWidgets) {
-            buttonWidget.getBounds().y = scrollListBounds.y + 1 + yOffset - (int) scrollAmount;
-            if (buttonWidget.getBounds().getMaxY() > scrollListBounds.getMinY() && buttonWidget.getBounds().getMinY() < scrollListBounds.getMaxY()) {
-                buttonWidget.render(mouseX, mouseY, delta);
+        for (Button button : buttonList) {
+            button.getBounds().y = scrollListBounds.y + 1 + yOffset - (int) scrollAmount;
+            if (button.getBounds().getMaxY() > scrollListBounds.getMinY() && button.getBounds().getMinY() < scrollListBounds.getMaxY()) {
+                button.render(mouseX, mouseY, delta);
             }
-            yOffset += buttonWidget.getBounds().height;
+            yOffset += button.getBounds().height;
         }
-        for (int i = 0; i < buttonWidgets.size(); i++) {
-            if (buttonWidgets.get(i).getBounds().getMaxY() > scrollListBounds.getMinY() && buttonWidgets.get(i).getBounds().getMinY() < scrollListBounds.getMaxY()) {
+        for (int i = 0; i < buttonList.size(); i++) {
+            if (buttonList.get(i).getBounds().getMaxY() > scrollListBounds.getMinY() && buttonList.get(i).getBounds().getMinY() < scrollListBounds.getMaxY()) {
                 recipeRenderers.get(i).setZ(1);
-                recipeRenderers.get(i).render(buttonWidgets.get(i).getBounds(), mouseX, mouseY, delta);
-                recipeRenderers.get(i).getTooltip(new Point(mouseX, mouseY)).queue();
+                recipeRenderers.get(i).render(buttonList.get(i).getBounds(), mouseX, mouseY, delta);
+                Optional.ofNullable(recipeRenderers.get(i).getTooltip(new Point(mouseX, mouseY))).ifPresent(Tooltip::queue);
             }
         }
         double maxScroll = getMaxScrollPosition();
@@ -439,7 +423,7 @@ public class VillagerRecipeViewingScreen extends Screen implements RecipeScreen
     @Override
     public boolean mouseDragged(double mouseX, double mouseY, int int_1, double double_3, double double_4) {
         if (int_1 == 0 && scrollBarAlpha > 0 && draggingScrollBar) {
-            double height = CollectionUtils.sumInt(buttonWidgets, b -> b.getBounds().getHeight());
+            double height = CollectionUtils.sumInt(buttonList, b -> b.getBounds().getHeight());
             int actualHeight = scrollListBounds.height - 2;
             if (height > actualHeight && mouseY >= scrollListBounds.y + 1 && mouseY <= scrollListBounds.getMaxY() - 1) {
                 int int_3 = MathHelper.clamp((int) ((actualHeight * actualHeight) / height), 32, actualHeight - 8);

+ 0 - 192
src/main/java/me/shedaniel/rei/gui/widget/AutoCraftingButtonWidget.java

@@ -1,192 +0,0 @@
-/*
- * 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.gui.widget;
-
-import com.google.common.collect.Lists;
-import it.unimi.dsi.fastutil.ints.IntList;
-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.Tooltip;
-import me.shedaniel.rei.gui.toast.CopyRecipeIdentifierToast;
-import me.shedaniel.rei.impl.ClientHelperImpl;
-import me.shedaniel.rei.impl.ScreenHelper;
-import me.shedaniel.rei.utils.CollectionUtils;
-import net.minecraft.client.gui.screen.ingame.HandledScreen;
-import net.minecraft.client.resource.language.I18n;
-import net.minecraft.text.LiteralText;
-import net.minecraft.util.Formatting;
-import net.minecraft.util.Identifier;
-import org.jetbrains.annotations.ApiStatus;
-
-import java.util.List;
-import java.util.Optional;
-import java.util.function.Supplier;
-
-@ApiStatus.Internal
-public class AutoCraftingButtonWidget extends ButtonWidget {
-    
-    private final Supplier<RecipeDisplay> displaySupplier;
-    private String extraTooltip;
-    private List<String> errorTooltip;
-    private List<Widget> setupDisplay;
-    private HandledScreen<?> handledScreen;
-    private boolean visible = false;
-    private RecipeCategory<?> category;
-    private Rectangle displayBounds;
-    
-    public AutoCraftingButtonWidget(Rectangle displayBounds, Rectangle rectangle, String text, Supplier<RecipeDisplay> displaySupplier, List<Widget> setupDisplay, RecipeCategory<?> recipeCategory) {
-        super(rectangle, new LiteralText(text));
-        this.displayBounds = displayBounds;
-        this.displaySupplier = displaySupplier;
-        Optional<Identifier> recipe = displaySupplier.get().getRecipeLocation();
-        extraTooltip = recipe.isPresent() ? I18n.translate("text.rei.recipe_id", Formatting.GRAY.toString(), recipe.get().toString()) : "";
-        this.handledScreen = ScreenHelper.getLastHandledScreen();
-        this.setupDisplay = setupDisplay;
-        this.category = recipeCategory;
-    }
-    
-    @Override
-    public void onPressed() {
-        AutoTransferHandler.Context context = AutoTransferHandler.Context.create(true, handledScreen, displaySupplier.get());
-        for (AutoTransferHandler autoTransferHandler : RecipeHelper.getInstance().getSortedAutoCraftingHandler())
-            try {
-                AutoTransferHandler.Result result = autoTransferHandler.handle(context);
-                if (result.isSuccessful())
-                    return;
-            } catch (Exception e) {
-                e.printStackTrace();
-            }
-        minecraft.openScreen(handledScreen);
-        ScreenHelper.getLastOverlay().init();
-    }
-    
-    @Override
-    public void render(int mouseX, int mouseY, float delta) {
-        this.enabled = false;
-        List<String> error = null;
-        int color = 0;
-        visible = false;
-        IntList redSlots = null;
-        AutoTransferHandler.Context context = AutoTransferHandler.Context.create(false, handledScreen, displaySupplier.get());
-        for (AutoTransferHandler autoTransferHandler : RecipeHelper.getInstance().getSortedAutoCraftingHandler()) {
-            try {
-                AutoTransferHandler.Result result = autoTransferHandler.handle(context);
-                if (result.isApplicable())
-                    visible = true;
-                if (result.isSuccessful()) {
-                    enabled = true;
-                    error = null;
-                    color = 0;
-                    redSlots = null;
-                    break;
-                } else if (result.isApplicable()) {
-                    if (error == null) {
-                        error = Lists.newArrayList();
-                    }
-                    error.add(result.getErrorKey());
-                    color = result.getColor();
-                    if (result.getIntegers() != null && !result.getIntegers().isEmpty())
-                        redSlots = result.getIntegers();
-                }
-            } catch (Exception e) {
-                e.printStackTrace();
-            }
-        }
-        if (!visible) {
-            enabled = false;
-            if (error == null) {
-                error = Lists.newArrayList();
-            } else {
-                error.clear();
-            }
-            error.add("error.rei.no.handlers.applicable");
-        }
-        if (isHovered(mouseX, mouseY) && category instanceof TransferRecipeCategory && redSlots != null) {
-            ((TransferRecipeCategory<RecipeDisplay>) category).renderRedSlots(setupDisplay, displayBounds, displaySupplier.get(), redSlots);
-        }
-        errorTooltip = error == null || error.isEmpty() ? null : Lists.newArrayList();
-        if (errorTooltip != null) {
-            for (String s : error) {
-                if (errorTooltip.stream().noneMatch(ss -> ss.equalsIgnoreCase(s)))
-                    errorTooltip.add(s);
-            }
-        }
-        int x = getBounds().x, y = getBounds().y, width = getBounds().width, height = getBounds().height;
-        renderBackground(x, y, width, height, this.getTextureId(isHovered(mouseX, mouseY)));
-        
-        int colour = 14737632;
-        if (!this.visible) {
-            colour = 10526880;
-        } else if (enabled && isHovered(mouseX, mouseY)) {
-            colour = 16777120;
-        }
-        
-        fillGradient(x, y, x + width, y + height, color, color);
-        this.drawCenteredString(font, getText(), x + width / 2, y + (height - 8) / 2, colour);
-        
-        if (getTooltips().isPresent())
-            if (!focused && containsMouse(mouseX, mouseY))
-                Tooltip.create(getTooltips().get().split("\n")).queue();
-            else if (focused)
-                Tooltip.create(new Point(x + width / 2, y + height / 2), getTooltips().get().split("\n")).queue();
-    }
-    
-    @Override
-    protected int getTextureId(boolean boolean_1) {
-        return !visible ? 0 : boolean_1 && enabled ? 4 : 1;
-    }
-    
-    @Override
-    public Optional<String> getTooltips() {
-        String str = "";
-        if (errorTooltip == null) {
-            if (((ClientHelperImpl) ClientHelper.getInstance()).isYog.get())
-                str += I18n.translate("text.auto_craft.move_items.yog");
-            else
-                str += I18n.translate("text.auto_craft.move_items");
-        } else {
-            if (errorTooltip.size() > 1)
-                str += Formatting.RED.toString() + I18n.translate("error.rei.multi.errors") + "\n";
-            str += CollectionUtils.mapAndJoinToString(errorTooltip, s -> Formatting.RED.toString() + (errorTooltip.size() > 1 ? "- " : "") + I18n.translate(s), "\n");
-        }
-        if (this.minecraft.options.advancedItemTooltips) {
-            str += extraTooltip;
-        }
-        return Optional.of(str);
-    }
-    
-    @Override
-    public boolean keyPressed(int int_1, int int_2, int int_3) {
-        if (displaySupplier.get().getRecipeLocation().isPresent() && ConfigObject.getInstance().getCopyRecipeIdentifierKeybind().matchesKey(int_1, int_2) && containsMouse(PointHelper.ofMouse())) {
-            minecraft.keyboard.setClipboard(displaySupplier.get().getRecipeLocation().get().toString());
-            if (ConfigObject.getInstance().isToastDisplayedOnCopyIdentifier()) {
-                CopyRecipeIdentifierToast.addToast(I18n.translate("msg.rei.copied_recipe_id"), I18n.translate("msg.rei.recipe_id_details", displaySupplier.get().getRecipeLocation().get().toString()));
-            }
-            return true;
-        }
-        return super.keyPressed(int_1, int_2, int_3);
-    }
-}

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

@@ -35,6 +35,8 @@ import net.minecraft.text.LiteralText;
 import net.minecraft.text.Text;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.math.MathHelper;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.Collections;
 import java.util.List;
@@ -43,6 +45,12 @@ import java.util.Optional;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
 
+/**
+ * @see me.shedaniel.rei.api.widgets.Widgets#createButton(me.shedaniel.math.Rectangle, Text)
+ * @see me.shedaniel.rei.api.widgets.Widgets#createButton(me.shedaniel.math.Rectangle, String)
+ */
+@Deprecated
+@ApiStatus.ScheduledForRemoval
 public abstract class ButtonWidget extends WidgetWithBounds {
     
     protected static final Identifier BUTTON_LOCATION = new Identifier("roughlyenoughitems", "textures/gui/button.png");
@@ -93,6 +101,7 @@ public abstract class ButtonWidget extends WidgetWithBounds {
         return canChangeFocuses;
     }
     
+    @NotNull
     public Rectangle getBounds() {
         return bounds;
     }

+ 0 - 82
src/main/java/me/shedaniel/rei/gui/widget/CraftableToggleButtonWidget.java

@@ -1,82 +0,0 @@
-/*
- * 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.gui.widget;
-
-import me.shedaniel.math.api.Rectangle;
-import me.shedaniel.rei.api.ConfigManager;
-import net.minecraft.block.Blocks;
-import net.minecraft.client.render.item.ItemRenderer;
-import net.minecraft.client.resource.language.I18n;
-import net.minecraft.client.util.NarratorManager;
-import net.minecraft.item.ItemStack;
-import net.minecraft.util.Identifier;
-import org.jetbrains.annotations.ApiStatus;
-
-import java.util.Optional;
-
-@ApiStatus.Internal
-public abstract class CraftableToggleButtonWidget extends LateRenderedButton {
-    
-    public static final Identifier CHEST_GUI_TEXTURE = new Identifier("roughlyenoughitems", "textures/gui/recipecontainer.png");
-    private static final ItemStack ICON = new ItemStack(Blocks.CRAFTING_TABLE);
-    private ItemRenderer itemRenderer;
-    
-    public CraftableToggleButtonWidget(Rectangle rectangle) {
-        super(rectangle, NarratorManager.EMPTY);
-        this.itemRenderer = minecraft.getItemRenderer();
-    }
-    
-    public CraftableToggleButtonWidget(int x, int y, int width, int height) {
-        this(new Rectangle(x, y, width, height));
-    }
-    
-    @Override
-    public void lateRender(int mouseX, int mouseY, float delta) {
-        setZOffset(600);
-        super.render(mouseX, mouseY, delta);
-        
-        this.itemRenderer.zOffset = getZOffset() - 98;
-        Rectangle bounds = getBounds();
-        this.itemRenderer.renderGuiItemIcon(ICON, bounds.x + 2, bounds.y + 2);
-        this.itemRenderer.zOffset = 0.0F;
-        int color = ConfigManager.getInstance().isCraftableOnlyEnabled() ? 939579655 : 956235776;
-        setZOffset(getZOffset() + 1);
-        this.fillGradient(bounds.x + 1, bounds.y + 1, bounds.getMaxX() - 1, bounds.getMaxY() - 1, color, color);
-        setZOffset(0);
-    }
-    
-    @Override
-    public void render(int mouseX, int mouseY, float delta) {
-    }
-    
-    @Override
-    public boolean changeFocus(boolean boolean_1) {
-        return false;
-    }
-    
-    @Override
-    public Optional<String> getTooltips() {
-        return Optional.ofNullable(I18n.translate(ConfigManager.getInstance().isCraftableOnlyEnabled() ? "text.rei.showing_craftable" : "text.rei.showing_all"));
-    }
-}

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

@@ -54,6 +54,7 @@ import net.minecraft.item.ItemGroup;
 import net.minecraft.util.ActionResult;
 import net.minecraft.util.math.MathHelper;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import java.util.Collections;
@@ -186,6 +187,7 @@ public class EntryListWidget extends WidgetWithBounds {
         return super.mouseScrolled(double_1, double_2, double_3);
     }
     
+    @NotNull
     @Override
     public Rectangle getBounds() {
         return bounds;

+ 8 - 0
src/main/java/me/shedaniel/rei/gui/widget/EntryWidget.java

@@ -91,6 +91,7 @@ public class EntryWidget extends Slot {
     }
     
     @Override
+    @NotNull
     public EntryWidget unmarkInputOrOutput() {
         noticeMark = 0;
         return this;
@@ -140,6 +141,7 @@ public class EntryWidget extends Slot {
         return interactable(false);
     }
     
+    @NotNull
     @Override
     public EntryWidget interactable(boolean b) {
         interactable = b;
@@ -151,6 +153,7 @@ public class EntryWidget extends Slot {
         return interactableFavorites(false);
     }
     
+    @NotNull
     @Override
     public EntryWidget interactableFavorites(boolean b) {
         interactableFavorites = b && interactable;
@@ -219,17 +222,20 @@ public class EntryWidget extends Slot {
         return this;
     }
     
+    @NotNull
     @Override
     public Slot clearEntries() {
         return clearStacks();
     }
     
+    @NotNull
     @Override
     public EntryWidget entry(EntryStack stack) {
         entryStacks.add(stack);
         return this;
     }
     
+    @NotNull
     @Override
     public EntryWidget entries(Collection<EntryStack> stacks) {
         entryStacks.addAll(stacks);
@@ -244,6 +250,7 @@ public class EntryWidget extends Slot {
         return entryStacks.get(MathHelper.floor((System.currentTimeMillis() / 500 % (double) entryStacks.size()) / 1f));
     }
     
+    @NotNull
     @Override
     public List<EntryStack> getEntries() {
         return entryStacks;
@@ -253,6 +260,7 @@ public class EntryWidget extends Slot {
         return entryStacks;
     }
     
+    @NotNull
     @Override
     public Rectangle getBounds() {
         return bounds;

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

@@ -46,6 +46,7 @@ import net.minecraft.client.render.VertexFormats;
 import net.minecraft.client.resource.language.I18n;
 import net.minecraft.util.math.MathHelper;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import java.util.Collections;
@@ -115,6 +116,7 @@ public class FavoritesListWidget extends WidgetWithBounds {
         return super.mouseScrolled(double_1, double_2, double_3);
     }
     
+    @NotNull
     @Override
     public Rectangle getBounds() {
         return bounds;

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

@@ -30,6 +30,7 @@ import me.shedaniel.rei.api.widgets.Tooltip;
 import me.shedaniel.rei.api.widgets.Widgets;
 import net.minecraft.client.gui.Element;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.Collections;
 import java.util.List;
@@ -172,6 +173,7 @@ public class LabelWidget extends WidgetWithBounds {
         return Optional.ofNullable(tooltipSupplier).map(Supplier::get);
     }
     
+    @NotNull
     @Override
     public Rectangle getBounds() {
         int width = font.getStringWidth(text);

+ 0 - 35
src/main/java/me/shedaniel/rei/gui/widget/LateRenderedButton.java

@@ -1,35 +0,0 @@
-/*
- * 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.gui.widget;
-
-import me.shedaniel.math.api.Rectangle;
-import net.minecraft.text.Text;
-import org.jetbrains.annotations.ApiStatus;
-
-@ApiStatus.Internal
-public abstract class LateRenderedButton extends ButtonWidget implements LateRenderable {
-    protected LateRenderedButton(Rectangle rectangle, Text text) {
-        super(rectangle, text);
-    }
-}

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

@@ -31,6 +31,7 @@ import me.shedaniel.rei.gui.config.RecipeBorderType;
 import me.shedaniel.rei.gui.config.RecipeScreenType;
 import net.minecraft.util.Identifier;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.Collections;
 import java.util.List;
@@ -62,6 +63,7 @@ public class PanelWidget extends WidgetWithBounds {
         TEMP.render();
     }
     
+    @NotNull
     @Override
     public Rectangle getBounds() {
         return bounds;

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

@@ -29,6 +29,7 @@ import me.shedaniel.math.Point;
 import me.shedaniel.math.impl.PointHelper;
 import me.shedaniel.rei.api.widgets.Tooltip;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.Collection;
 import java.util.List;
@@ -47,34 +48,42 @@ public class QueuedTooltip implements Tooltip {
         this.text = Lists.newArrayList(text);
     }
     
+    @NotNull
     public static QueuedTooltip create(me.shedaniel.math.api.Point location, List<String> text) {
         return new QueuedTooltip(location, text);
     }
     
+    @NotNull
     public static QueuedTooltip create(me.shedaniel.math.api.Point location, String... text) {
         return QueuedTooltip.create(location, Lists.newArrayList(text));
     }
     
+    @NotNull
     public static QueuedTooltip create(Point location, List<String> text) {
         return new QueuedTooltip(location, text);
     }
     
+    @NotNull
     public static QueuedTooltip create(Point location, Collection<String> text) {
         return new QueuedTooltip(location, text);
     }
     
+    @NotNull
     public static QueuedTooltip create(Point location, String... text) {
         return QueuedTooltip.create(location, Lists.newArrayList(text));
     }
     
+    @NotNull
     public static QueuedTooltip create(List<String> text) {
         return QueuedTooltip.create(PointHelper.ofMouse(), text);
     }
     
+    @NotNull
     public static QueuedTooltip create(Collection<String> text) {
         return QueuedTooltip.create(PointHelper.ofMouse(), text);
     }
     
+    @NotNull
     public static QueuedTooltip create(String... text) {
         return QueuedTooltip.create(PointHelper.ofMouse(), text);
     }
@@ -85,6 +94,7 @@ public class QueuedTooltip implements Tooltip {
      */
     @Deprecated
     @ApiStatus.ScheduledForRemoval
+    @NotNull
     public me.shedaniel.math.api.Point getLocation() {
         return new me.shedaniel.math.api.Point(location);
     }

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

@@ -30,6 +30,7 @@ import net.minecraft.client.MinecraftClient;
 import net.minecraft.client.gui.Element;
 import net.minecraft.util.math.MathHelper;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.Collections;
 import java.util.List;
@@ -61,6 +62,7 @@ public class RecipeArrowWidget extends WidgetWithBounds {
         return this;
     }
     
+    @NotNull
     @Override
     public Rectangle getBounds() {
         return new Rectangle(x, y, 24, 17);

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

@@ -28,6 +28,7 @@ import com.mojang.blaze3d.systems.RenderSystem;
 import me.shedaniel.math.api.Point;
 import me.shedaniel.math.api.Rectangle;
 import me.shedaniel.rei.api.REIHelper;
+import me.shedaniel.rei.api.widgets.Button;
 import me.shedaniel.rei.api.widgets.Panel;
 import me.shedaniel.rei.api.widgets.Widgets;
 import me.shedaniel.rei.gui.RecipeViewingScreen;
@@ -37,6 +38,7 @@ import net.minecraft.client.util.Window;
 import net.minecraft.text.TranslatableText;
 import net.minecraft.util.math.MathHelper;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.Collections;
 import java.util.List;
@@ -52,7 +54,7 @@ public class RecipeChoosePageWidget extends DraggableWidget {
     private RecipeViewingScreen recipeViewingScreen;
     private TextFieldWidget textFieldWidget;
     private Panel base1, base2;
-    private ButtonWidget btnDone;
+    private Button btnDone;
     
     public RecipeChoosePageWidget(RecipeViewingScreen recipeViewingScreen, int currentPage, int maxPage) {
         super(getPointFromConfig());
@@ -67,6 +69,7 @@ public class RecipeChoosePageWidget extends DraggableWidget {
         return new Point(window.getScaledWidth() * .5, window.getScaledHeight() * .5);
     }
     
+    @NotNull
     @Override
     public Rectangle getBounds() {
         return bounds;
@@ -137,11 +140,12 @@ public class RecipeChoosePageWidget extends DraggableWidget {
             return stringBuilder_1.toString();
         };
         textFieldWidget.setText(String.valueOf(currentPage + 1));
-        widgets.add(btnDone = ButtonWidget.create(new Rectangle(bounds.x + bounds.width - 45, bounds.y + bounds.height + 3, 40, 20), new TranslatableText("gui.done"), buttonWidget -> {
-            recipeViewingScreen.page = MathHelper.clamp(getIntFromString(textFieldWidget.getText()).orElse(0) - 1, 0, recipeViewingScreen.getTotalPages(recipeViewingScreen.getSelectedCategory()) - 1);
-            recipeViewingScreen.choosePageActivated = false;
-            recipeViewingScreen.init();
-        }));
+        widgets.add(btnDone = Widgets.createButton(new Rectangle(bounds.x + bounds.width - 45, bounds.y + bounds.height + 3, 40, 20), new TranslatableText("gui.done"))
+                .onClick(button -> {
+                    recipeViewingScreen.page = MathHelper.clamp(getIntFromString(textFieldWidget.getText()).orElse(0) - 1, 0, recipeViewingScreen.getTotalPages(recipeViewingScreen.getSelectedCategory()) - 1);
+                    recipeViewingScreen.choosePageActivated = false;
+                    recipeViewingScreen.init();
+                }));
         textFieldWidget.setFocused(true);
     }
     

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

@@ -32,6 +32,7 @@ import me.shedaniel.rei.api.widgets.Tooltip;
 import net.minecraft.util.Formatting;
 import net.minecraft.util.Identifier;
 import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 import java.util.Collections;
@@ -122,6 +123,7 @@ public class TabWidget extends WidgetWithBounds {
             Tooltip.create(categoryName, ClientHelper.getInstance().getFormattedModFromIdentifier(category.getIdentifier())).queue();
     }
     
+    @NotNull
     @Override
     public Rectangle getBounds() {
         return bounds;

+ 7 - 0
src/main/java/me/shedaniel/rei/gui/widget/TextFieldWidget.java

@@ -34,6 +34,8 @@ import net.minecraft.client.render.Tessellator;
 import net.minecraft.client.render.VertexFormats;
 import net.minecraft.util.Tickable;
 import net.minecraft.util.math.MathHelper;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.Collections;
 import java.util.List;
@@ -42,6 +44,10 @@ import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
+/**
+ * @see net.minecraft.client.gui.widget.TextFieldWidget
+ */
+@ApiStatus.Internal
 public class TextFieldWidget extends WidgetWithBounds implements Tickable {
     
     public Function<String, String> stripInvalid;
@@ -93,6 +99,7 @@ public class TextFieldWidget extends WidgetWithBounds implements Tickable {
         this.suggestion = string_1;
     }
     
+    @NotNull
     @Override
     public Rectangle getBounds() {
         return bounds;

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

@@ -24,9 +24,11 @@
 package me.shedaniel.rei.gui.widget;
 
 import me.shedaniel.math.api.Rectangle;
+import org.jetbrains.annotations.NotNull;
 
 public abstract class WidgetWithBounds extends Widget {
     
+    @NotNull
     public abstract Rectangle getBounds();
     
     @Override

+ 242 - 0
src/main/java/me/shedaniel/rei/impl/InternalWidgets.java

@@ -0,0 +1,242 @@
+package me.shedaniel.rei.impl;
+
+import com.google.common.collect.Lists;
+import it.unimi.dsi.fastutil.ints.IntList;
+import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.math.impl.PointHelper;
+import me.shedaniel.rei.api.*;
+import me.shedaniel.rei.api.widgets.Button;
+import me.shedaniel.rei.api.widgets.Widgets;
+import me.shedaniel.rei.gui.toast.CopyRecipeIdentifierToast;
+import me.shedaniel.rei.gui.widget.LateRenderable;
+import me.shedaniel.rei.gui.widget.Widget;
+import me.shedaniel.rei.gui.widget.WidgetWithBounds;
+import me.shedaniel.rei.utils.CollectionUtils;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.screen.ingame.HandledScreen;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.util.Formatting;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+@ApiStatus.Internal
+public final class InternalWidgets {
+    private InternalWidgets() {}
+    
+    public static Widget createAutoCraftingButtonWidget(Rectangle displayBounds, me.shedaniel.math.Rectangle rectangle, String text, Supplier<RecipeDisplay> displaySupplier, List<Widget> setupDisplay, RecipeCategory<?> category) {
+        HandledScreen<?> handledScreen = ScreenHelper.getLastHandledScreen();
+        boolean[] visible = {false};
+        List<String>[] errorTooltip = new List[]{null};
+        Button autoCraftingButton = Widgets.createButton(rectangle, text)
+                .focusable(false)
+                .onClick(button -> {
+                    AutoTransferHandler.Context context = AutoTransferHandler.Context.create(true, handledScreen, displaySupplier.get());
+                    for (AutoTransferHandler autoTransferHandler : RecipeHelper.getInstance().getSortedAutoCraftingHandler())
+                        try {
+                            AutoTransferHandler.Result result = autoTransferHandler.handle(context);
+                            if (result.isSuccessful())
+                                return;
+                        } catch (Exception e) {
+                            e.printStackTrace();
+                        }
+                    MinecraftClient.getInstance().openScreen(handledScreen);
+                    ScreenHelper.getLastOverlay().init();
+                })
+                .onRender(button -> {
+                    button.setEnabled(false);
+                    List<String> error = null;
+                    int color = 0;
+                    visible[0] = false;
+                    IntList redSlots = null;
+                    AutoTransferHandler.Context context = AutoTransferHandler.Context.create(false, handledScreen, displaySupplier.get());
+                    for (AutoTransferHandler autoTransferHandler : RecipeHelper.getInstance().getSortedAutoCraftingHandler()) {
+                        try {
+                            AutoTransferHandler.Result result = autoTransferHandler.handle(context);
+                            if (result.isApplicable())
+                                visible[0] = true;
+                            if (result.isSuccessful()) {
+                                button.setEnabled(true);
+                                error = null;
+                                color = 0;
+                                redSlots = null;
+                                break;
+                            } else if (result.isApplicable()) {
+                                if (error == null) {
+                                    error = Lists.newArrayList();
+                                }
+                                error.add(result.getErrorKey());
+                                color = result.getColor();
+                                if (result.getIntegers() != null && !result.getIntegers().isEmpty())
+                                    redSlots = result.getIntegers();
+                            }
+                        } catch (Exception e) {
+                            e.printStackTrace();
+                        }
+                    }
+                    if (!visible[0]) {
+                        button.setEnabled(false);
+                        if (error == null) {
+                            error = Lists.newArrayList();
+                        } else {
+                            error.clear();
+                        }
+                        error.add("error.rei.no.handlers.applicable");
+                    }
+                    if ((button.containsMouse(PointHelper.ofMouse()) || button.isFocused()) && category instanceof TransferRecipeCategory && redSlots != null) {
+                        ((TransferRecipeCategory<RecipeDisplay>) category).renderRedSlots(setupDisplay, displayBounds, displaySupplier.get(), redSlots);
+                    }
+                    errorTooltip[0] = error == null || error.isEmpty() ? null : Lists.newArrayList();
+                    if (errorTooltip[0] != null) {
+                        for (String s : error) {
+                            if (errorTooltip[0].stream().noneMatch(ss -> ss.equalsIgnoreCase(s)))
+                                errorTooltip[0].add(s);
+                        }
+                    }
+                    button.setTint(color);
+                })
+                .textColor((button, mouse) -> {
+                    if (!visible[0]) {
+                        return 10526880;
+                    } else if (button.isEnabled() && (button.containsMouse(mouse) || button.isFocused())) {
+                        return 16777120;
+                    }
+                    return 14737632;
+                })
+                .textureId((button, mouse) -> !visible[0] ? 0 : (button.containsMouse(mouse) || button.isFocused()) && button.isEnabled() ? 4 : 1)
+                .tooltipSupplier(button -> {
+                    String str = "";
+                    if (errorTooltip[0] == null) {
+                        if (((ClientHelperImpl) ClientHelper.getInstance()).isYog.get())
+                            str += I18n.translate("text.auto_craft.move_items.yog");
+                        else
+                            str += I18n.translate("text.auto_craft.move_items");
+                    } else {
+                        if (errorTooltip[0].size() > 1)
+                            str += Formatting.RED.toString() + I18n.translate("error.rei.multi.errors") + "\n";
+                        str += CollectionUtils.mapAndJoinToString(errorTooltip[0], s -> Formatting.RED.toString() + (errorTooltip[0].size() > 1 ? "- " : "") + I18n.translate(s), "\n");
+                    }
+                    if (MinecraftClient.getInstance().options.advancedItemTooltips) {
+                        str += displaySupplier.get().getRecipeLocation().isPresent() ? I18n.translate("text.rei.recipe_id", Formatting.GRAY.toString(), displaySupplier.get().getRecipeLocation().get().toString()) : "";
+                    }
+                    return str;
+                });
+        return new WidgetWithBounds() {
+            @Override
+            public @NotNull Rectangle getBounds() {
+                return autoCraftingButton.getBounds();
+            }
+            
+            @Override
+            public List<? extends Element> children() {
+                return Collections.singletonList(autoCraftingButton);
+            }
+            
+            @Override
+            public void render(int mouseX, int mouseY, float delta) {
+                autoCraftingButton.render(mouseX, mouseY, delta);
+            }
+            
+            @Override
+            public boolean keyPressed(int int_1, int int_2, int int_3) {
+                if (displaySupplier.get().getRecipeLocation().isPresent() && ConfigObject.getInstance().getCopyRecipeIdentifierKeybind().matchesKey(int_1, int_2) && containsMouse(PointHelper.ofMouse())) {
+                    minecraft.keyboard.setClipboard(displaySupplier.get().getRecipeLocation().get().toString());
+                    if (ConfigObject.getInstance().isToastDisplayedOnCopyIdentifier()) {
+                        CopyRecipeIdentifierToast.addToast(I18n.translate("msg.rei.copied_recipe_id"), I18n.translate("msg.rei.recipe_id_details", displaySupplier.get().getRecipeLocation().get().toString()));
+                    }
+                    return true;
+                }
+                return super.keyPressed(int_1, int_2, int_3);
+            }
+        };
+    }
+    
+    public static LateRenderable wrapLateRenderable(WidgetWithBounds widget) {
+        return new LateRenderableWidgetWithBounds(widget);
+    }
+    
+    public static LateRenderable wrapLateRenderable(Widget widget) {
+        return new LateRenderableWidget(widget);
+    }
+    
+    public static Widget mergeWidgets(Widget widget1, Widget widget2) {
+        return new MergedWidget(widget2, widget1);
+    }
+    
+    private static class MergedWidget extends Widget {
+        private final List<Widget> widgets;
+        
+        public MergedWidget(Widget widget1, Widget widget2) {
+            this.widgets = Lists.newArrayList(Objects.requireNonNull(widget1), Objects.requireNonNull(widget2));
+        }
+        
+        @Override
+        public void render(int mouseX, int mouseY, float delta) {
+            for (Widget widget : widgets) {
+                widget.setZ(getZ());
+                widget.render(mouseX, mouseY, delta);
+            }
+        }
+        
+        @Override
+        public List<? extends Element> children() {
+            return widgets;
+        }
+    }
+    
+    private static class LateRenderableWidget extends Widget implements LateRenderable {
+        private final Widget widget;
+        
+        private LateRenderableWidget(Widget widget) {
+            this.widget = widget;
+        }
+        
+        
+        @Override
+        public void lateRender(int mouseX, int mouseY, float delta) {
+            this.widget.setZ(getZ());
+            this.widget.render(mouseX, mouseY, delta);
+        }
+        
+        @Override
+        public void render(int mouseX, int mouseY, float delta) {}
+        
+        @Override
+        public List<? extends Element> children() {
+            return Collections.singletonList(this.widget);
+        }
+    }
+    
+    private static class LateRenderableWidgetWithBounds extends WidgetWithBounds implements LateRenderable {
+        private final WidgetWithBounds widget;
+        
+        private LateRenderableWidgetWithBounds(WidgetWithBounds widget) {
+            this.widget = widget;
+        }
+        
+        
+        @Override
+        public void lateRender(int mouseX, int mouseY, float delta) {
+            this.widget.setZ(getZ());
+            this.widget.render(mouseX, mouseY, delta);
+        }
+        
+        @Override
+        public @NotNull Rectangle getBounds() {
+            return this.widget.getBounds();
+        }
+        
+        @Override
+        public void render(int mouseX, int mouseY, float delta) {}
+        
+        @Override
+        public List<? extends Element> children() {
+            return Collections.singletonList(this.widget);
+        }
+    }
+}

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

@@ -211,6 +211,9 @@ public class ScreenHelper implements ClientModInitializer, REIHelper {
         consumer.accept(actualX, actualY, delta);
     }
     
+    /**
+     * @deprecated Please switch to {@link REIHelper#isDarkThemeEnabled()}
+     */
     @Deprecated
     @ApiStatus.Internal
     @ApiStatus.ScheduledForRemoval

+ 3 - 2
src/main/java/me/shedaniel/rei/impl/widgets/ArrowWidget.java

@@ -35,7 +35,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 
-public class ArrowWidget extends Arrow {
+public final class ArrowWidget extends Arrow {
     @NotNull
     private Rectangle bounds;
     private double animationDuration = -1;
@@ -56,8 +56,9 @@ public class ArrowWidget extends Arrow {
             this.animationDuration = -1;
     }
     
+    @NotNull
     @Override
-    public @NotNull Rectangle getBounds() {
+    public Rectangle getBounds() {
         return bounds;
     }
     

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

@@ -35,7 +35,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 
-public class BurningFireWidget extends BurningFire {
+public final class BurningFireWidget extends BurningFire {
     @NotNull
     private Rectangle bounds;
     private double animationDuration = -1;

+ 284 - 0
src/main/java/me/shedaniel/rei/impl/widgets/ButtonWidget.java

@@ -0,0 +1,284 @@
+package me.shedaniel.rei.impl.widgets;
+
+import com.mojang.blaze3d.systems.RenderSystem;
+import me.shedaniel.math.api.Point;
+import me.shedaniel.math.api.Rectangle;
+import me.shedaniel.rei.api.REIHelper;
+import me.shedaniel.rei.api.widgets.Button;
+import me.shedaniel.rei.api.widgets.Tooltip;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.sound.PositionedSoundInstance;
+import net.minecraft.sound.SoundEvents;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.MathHelper;
+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.OptionalInt;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+public class ButtonWidget extends Button {
+    private static final Identifier BUTTON_LOCATION = new Identifier("roughlyenoughitems", "textures/gui/button.png");
+    private static final Identifier BUTTON_LOCATION_DARK = new Identifier("roughlyenoughitems", "textures/gui/button_dark.png");
+    @NotNull
+    private Rectangle bounds;
+    private boolean enabled = true;
+    @NotNull
+    private String text;
+    @Nullable
+    private Integer tint;
+    @Nullable
+    private Consumer<Button> onClick;
+    @Nullable
+    private Consumer<Button> onRender;
+    private boolean focusable = false;
+    private boolean focused = false;
+    @Nullable
+    private Function<@NotNull Button, @Nullable String> tooltipFunction;
+    @Nullable
+    private BiFunction<@NotNull Button, @NotNull Point, @NotNull Integer> textColorFunction;
+    @Nullable
+    private BiFunction<@NotNull Button, @NotNull Point, @NotNull Integer> textureIdFunction;
+    
+    public ButtonWidget(me.shedaniel.math.Rectangle rectangle, Text text) {
+        this(rectangle, Objects.requireNonNull(text).asFormattedString());
+    }
+    
+    public ButtonWidget(me.shedaniel.math.Rectangle rectangle, String text) {
+        this.bounds = new Rectangle(Objects.requireNonNull(rectangle));
+        this.text = Objects.requireNonNull(text);
+    }
+    
+    @Override
+    public final boolean isFocused() {
+        return focused;
+    }
+    
+    @Override
+    public final boolean isEnabled() {
+        return enabled;
+    }
+    
+    @Override
+    public final void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+    
+    @Override
+    public final OptionalInt getTint() {
+        return OptionalInt.empty();
+    }
+    
+    @Override
+    public final void setTint(int tint) {
+        this.tint = tint;
+    }
+    
+    @Override
+    public final void removeTint() {
+        this.tint = null;
+    }
+    
+    @Override
+    @NotNull
+    public final String getText() {
+        return text;
+    }
+    
+    @Override
+    public final void setText(@NotNull String text) {
+        this.text = text;
+    }
+    
+    @Override
+    public final @Nullable Consumer<Button> getOnClick() {
+        return onClick;
+    }
+    
+    @Override
+    public final void setOnClick(@Nullable Consumer<Button> onClick) {
+        this.onClick = onClick;
+    }
+    
+    @Override
+    public final @Nullable Consumer<Button> getOnRender() {
+        return onRender;
+    }
+    
+    @Override
+    public final void setOnRender(@Nullable Consumer<Button> onRender) {
+        this.onRender = onRender;
+    }
+    
+    @Override
+    public final boolean isFocusable() {
+        return focusable;
+    }
+    
+    @Override
+    public final void setFocusable(boolean focusable) {
+        this.focusable = focusable;
+    }
+    
+    @Override
+    public final @Nullable String getTooltip() {
+        if (tooltipFunction == null)
+            return null;
+        return tooltipFunction.apply(this);
+    }
+    
+    @Override
+    public final void setTooltip(@Nullable Function<@NotNull Button, @Nullable String> tooltip) {
+        this.tooltipFunction = tooltip;
+    }
+    
+    @Override
+    public final void setTextColor(@Nullable BiFunction<@NotNull Button, @NotNull Point, @NotNull Integer> textColorFunction) {
+        this.textColorFunction = textColorFunction;
+    }
+    
+    @Override
+    public final void setTextureId(@Nullable BiFunction<@NotNull Button, @NotNull Point, @NotNull Integer> textureIdFunction) {
+        this.textureIdFunction = textureIdFunction;
+    }
+    
+    @Override
+    public final int getTextColor(Point mouse) {
+        if (this.textColorFunction != null) {
+            Integer apply = this.textColorFunction.apply(this, mouse);
+            if (apply != null)
+                return apply;
+        }
+        if (!this.enabled) {
+            return 10526880;
+        } else if (isFocused(mouse.x, mouse.y)) {
+            return 16777120;
+        }
+        return 14737632;
+    }
+    
+    @Override
+    public final @NotNull Rectangle getBounds() {
+        return bounds;
+    }
+    
+    @Override
+    public void render(int mouseX, int mouseY, float delta) {
+        if (onRender != null) {
+            onRender.accept(this);
+        }
+        int x = bounds.x, y = bounds.y, width = bounds.width, height = bounds.height;
+        renderBackground(x, y, width, height, this.getTextureId(new Point(mouseX, mouseY)));
+        
+        int color = 14737632;
+        if (!this.enabled) {
+            color = 10526880;
+        } else if (isFocused(mouseX, mouseY)) {
+            color = 16777120;
+        }
+        
+        if (tint != null)
+            fillGradient(x + 1, y + 1, x + width - 1, y + height - 1, tint, tint);
+        
+        this.drawCenteredString(font, getText(), x + width / 2, y + (height - 8) / 2, color);
+        
+        String tooltip = getTooltip();
+        if (tooltip != null)
+            if (!focused && containsMouse(mouseX, mouseY))
+                Tooltip.create(tooltip.split("\n")).queue();
+            else if (focused)
+                Tooltip.create(new Point(x + width / 2, y + height / 2), tooltip.split("\n")).queue();
+    }
+    
+    protected boolean isFocused(int mouseX, int mouseY) {
+        return containsMouse(mouseX, mouseY) || focused;
+    }
+    
+    @Override
+    public boolean changeFocus(boolean boolean_1) {
+        if (!enabled || !focusable)
+            return false;
+        this.focused = !this.focused;
+        return true;
+    }
+    
+    @Override
+    public void onClick() {
+        Consumer<Button> onClick = getOnClick();
+        if (onClick != null)
+            onClick.accept(this);
+    }
+    
+    @Override
+    public boolean mouseClicked(double mouseX, double mouseY, int button) {
+        if (containsMouse(mouseX, mouseY) && isEnabled() && button == 0) {
+            minecraft.getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
+            onClick();
+            return true;
+        }
+        return false;
+    }
+    
+    @Override
+    public boolean keyPressed(int int_1, int int_2, int int_3) {
+        if (this.isEnabled() && focused) {
+            if (int_1 != 257 && int_1 != 32 && int_1 != 335) {
+                return false;
+            } else {
+                minecraft.getSoundManager().play(PositionedSoundInstance.master(SoundEvents.UI_BUTTON_CLICK, 1.0F));
+                onClick();
+                return true;
+            }
+        }
+        return false;
+    }
+    
+    @Override
+    public List<? extends Element> children() {
+        return Collections.emptyList();
+    }
+    
+    @Override
+    public final int getTextureId(Point mouse) {
+        if (this.textureIdFunction != null) {
+            Integer apply = this.textureIdFunction.apply(this, mouse);
+            if (apply != null)
+                return apply;
+        }
+        if (!this.isEnabled()) {
+            return 0;
+        } else if (containsMouse(mouse) || focused) {
+            return 4; // 2 is the old blue highlight, 3 is the 1.15 outline, 4 is the 1.15 online + light hover
+        }
+        return 1;
+    }
+    
+    protected void renderBackground(int x, int y, int width, int height, int textureOffset) {
+        minecraft.getTextureManager().bindTexture(REIHelper.getInstance().isDarkThemeEnabled() ? BUTTON_LOCATION_DARK : BUTTON_LOCATION);
+        RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
+        RenderSystem.enableBlend();
+        RenderSystem.blendFuncSeparate(770, 771, 1, 0);
+        RenderSystem.blendFunc(770, 771);
+        //Four Corners
+        blit(x, y, getZOffset(), 0, textureOffset * 80, 4, 4, 512, 256);
+        blit(x + width - 4, y, getZOffset(), 252, textureOffset * 80, 4, 4, 512, 256);
+        blit(x, y + height - 4, getZOffset(), 0, textureOffset * 80 + 76, 4, 4, 512, 256);
+        blit(x + width - 4, y + height - 4, getZOffset(), 252, textureOffset * 80 + 76, 4, 4, 512, 256);
+        
+        //Sides
+        blit(x + 4, y, getZOffset(), 4, textureOffset * 80, MathHelper.ceil((width - 8) / 2f), 4, 512, 256);
+        blit(x + 4, y + height - 4, getZOffset(), 4, textureOffset * 80 + 76, MathHelper.ceil((width - 8) / 2f), 4, 512, 256);
+        blit(x + 4 + MathHelper.ceil((width - 8) / 2f), y + height - 4, getZOffset(), 252 - MathHelper.floor((width - 8) / 2f), textureOffset * 80 + 76, MathHelper.floor((width - 8) / 2f), 4, 512, 256);
+        blit(x + 4 + MathHelper.ceil((width - 8) / 2f), y, getZOffset(), 252 - MathHelper.floor((width - 8) / 2f), textureOffset * 80, MathHelper.floor((width - 8) / 2f), 4, 512, 256);
+        for (int i = y + 4; i < y + height - 4; i += 76) {
+            blit(x, i, getZOffset(), 0, 4 + textureOffset * 80, MathHelper.ceil(width / 2f), MathHelper.clamp(y + height - 4 - i, 0, 76), 512, 256);
+            blit(x + MathHelper.ceil(width / 2f), i, getZOffset(), 256 - MathHelper.floor(width / 2f), 4 + textureOffset * 80, MathHelper.floor(width / 2f), MathHelper.clamp(y + height - 4 - i, 0, 76), 512, 256);
+        }
+    }
+}

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

@@ -32,7 +32,7 @@ import net.minecraft.client.render.Tessellator;
 import net.minecraft.client.render.VertexFormats;
 import org.jetbrains.annotations.NotNull;
 
-public class FillRectangleDrawableConsumer implements DrawableConsumer {
+public final class FillRectangleDrawableConsumer implements DrawableConsumer {
     @NotNull
     private Rectangle rectangle;
     private int color;

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

@@ -174,6 +174,7 @@ public final class LabelWidget extends Label {
         this.text = Objects.requireNonNull(text);
     }
     
+    @NotNull
     @Override
     public Rectangle getBounds() {
         int width = font.getStringWidth(text);

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

@@ -116,6 +116,7 @@ public final class PanelWidget extends Panel {
         this.rendering = Objects.requireNonNull(rendering);
     }
     
+    @NotNull
     @Override
     public me.shedaniel.math.api.Rectangle getBounds() {
         return bounds;

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

@@ -33,7 +33,7 @@ import net.minecraft.util.Identifier;
 import org.jetbrains.annotations.NotNull;
 import org.lwjgl.opengl.GL11;
 
-public class TexturedDrawableConsumer implements DrawableConsumer {
+public final class TexturedDrawableConsumer implements DrawableConsumer {
     
     @NotNull
     private Identifier identifier;

+ 2 - 0
src/main/java/me/shedaniel/rei/plugin/beacon/DefaultBeaconBaseCategory.java

@@ -49,6 +49,7 @@ import net.minecraft.client.render.VertexFormats;
 import net.minecraft.client.resource.language.I18n;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.math.MathHelper;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.List;
 
@@ -160,6 +161,7 @@ public class DefaultBeaconBaseCategory implements RecipeCategory<DefaultBeaconBa
             return MathHelper.ceil(widgets.size() / 8f) * 18;
         }
         
+        @NotNull
         @Override
         public me.shedaniel.math.api.Rectangle getBounds() {
             return bounds;

+ 2 - 0
src/main/java/me/shedaniel/rei/plugin/information/DefaultInformationCategory.java

@@ -52,6 +52,7 @@ import net.minecraft.client.util.math.Matrix4f;
 import net.minecraft.text.Text;
 import net.minecraft.util.Identifier;
 import net.minecraft.util.math.MathHelper;
+import org.jetbrains.annotations.NotNull;
 
 import java.util.Collections;
 import java.util.List;
@@ -193,6 +194,7 @@ public class DefaultInformationCategory implements RecipeCategory<DefaultInforma
             return i;
         }
         
+        @NotNull
         @Override
         public me.shedaniel.math.api.Rectangle getBounds() {
             return bounds;