aboutsummaryrefslogtreecommitdiff
path: root/runtime-frontend/favorites-entries/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'runtime-frontend/favorites-entries/src/main')
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/FavoritesListWidgetImpl.java259
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/history/DisplayEntry.java322
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/history/DisplayHistoryManager.java147
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/history/DisplayHistoryWidget.java458
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/listeners/FavoritesRegionListener.java74
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/panel/FadingFavoritesPanelButton.java126
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/panel/FavoritesPanel.java186
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/panel/FavoritesTogglePanelButton.java75
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/panel/rows/FavoritesPanelEmptyRow.java53
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/panel/rows/FavoritesPanelEntriesRow.java222
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/panel/rows/FavoritesPanelRow.java34
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/panel/rows/FavoritesPanelSectionRow.java62
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/panel/rows/FavoritesPanelSeparatorRow.java48
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/trash/TrashWidget.java110
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/plugin/FavoritesEntriesBuiltinPlugin.java52
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/region/EntryStacksRegionWidget.java504
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/region/RealRegionEntry.java90
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/region/RegionDraggableStack.java88
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/region/RegionEntryWidget.java89
-rw-r--r--runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/region/RegionListener.java76
20 files changed, 3075 insertions, 0 deletions
diff --git a/runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/FavoritesListWidgetImpl.java b/runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/FavoritesListWidgetImpl.java
new file mode 100644
index 000000000..ecc652dd1
--- /dev/null
+++ b/runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/FavoritesListWidgetImpl.java
@@ -0,0 +1,259 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020, 2021, 2022 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl.client.gui.widget.favorites;
+
+import com.google.common.collect.ImmutableList;
+import com.mojang.blaze3d.vertex.PoseStack;
+import me.shedaniel.math.Point;
+import me.shedaniel.math.Rectangle;
+import me.shedaniel.rei.api.client.REIRuntime;
+import me.shedaniel.rei.api.client.config.ConfigObject;
+import me.shedaniel.rei.api.client.favorites.FavoriteEntry;
+import me.shedaniel.rei.api.client.gui.drag.DraggedAcceptorResult;
+import me.shedaniel.rei.api.client.gui.drag.DraggingContext;
+import me.shedaniel.rei.api.client.gui.drag.component.DraggableComponent;
+import me.shedaniel.rei.api.client.gui.drag.component.DraggableComponentProviderWidget;
+import me.shedaniel.rei.api.client.gui.drag.component.DraggableComponentVisitorWidget;
+import me.shedaniel.rei.api.client.gui.widgets.Widget;
+import me.shedaniel.rei.api.client.gui.widgets.WidgetWithBounds;
+import me.shedaniel.rei.api.client.overlay.OverlayListWidget;
+import me.shedaniel.rei.api.client.registry.screen.ScreenRegistry;
+import me.shedaniel.rei.api.common.display.Display;
+import me.shedaniel.rei.api.common.entry.EntryStack;
+import me.shedaniel.rei.api.common.util.CollectionUtils;
+import me.shedaniel.rei.impl.client.config.ConfigManagerInternal;
+import me.shedaniel.rei.impl.client.gui.overlay.entries.FavoritesListWidget;
+import me.shedaniel.rei.impl.client.gui.overlay.widgets.ScaleIndicatorWidget;
+import me.shedaniel.rei.impl.client.gui.widget.favorites.history.DisplayHistoryManager;
+import me.shedaniel.rei.impl.client.gui.widget.favorites.history.DisplayHistoryWidget;
+import me.shedaniel.rei.impl.client.gui.widget.favorites.listeners.FavoritesRegionListener;
+import me.shedaniel.rei.impl.client.gui.widget.favorites.panel.FavoritesPanel;
+import me.shedaniel.rei.impl.client.gui.widget.favorites.panel.FavoritesTogglePanelButton;
+import me.shedaniel.rei.impl.client.gui.widget.favorites.trash.TrashWidget;
+import me.shedaniel.rei.impl.client.gui.widget.region.EntryStacksRegionWidget;
+import me.shedaniel.rei.impl.common.util.RectangleUtils;
+import net.minecraft.client.gui.screens.Screen;
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Stream;
+
+@ApiStatus.Internal
+public class FavoritesListWidgetImpl extends WidgetWithBounds implements FavoritesListWidget, DraggableComponentProviderWidget<Object>, DraggableComponentVisitorWidget, OverlayListWidget {
+ public Rectangle fullBounds;
+ public Rectangle excludedBounds;
+ public Rectangle favoritesBounds;
+ private final EntryStacksRegionWidget<FavoriteEntry> region = new EntryStacksRegionWidget<>(new FavoritesRegionListener(this));
+
+ public final FavoritesPanel favoritePanel = new FavoritesPanel(this, region);
+ public final TrashWidget trash = new TrashWidget(this);
+ public final DisplayHistoryWidget displayHistory = new DisplayHistoryWidget(this);
+ public final FavoritesTogglePanelButton togglePanelButton = new FavoritesTogglePanelButton(this);
+ private final List<Widget> children = ImmutableList.of(favoritePanel, togglePanelButton, region);
+ private final ScaleIndicatorWidget scaleIndicator = new ScaleIndicatorWidget();
+
+ @Override
+ public boolean mouseScrolled(double mouseX, double mouseY, double amount) {
+ if (fullBounds.contains(mouseX, mouseY)) {
+ if (Screen.hasControlDown()) {
+ ConfigManagerInternal manager = ConfigManagerInternal.getInstance();
+ manager.set("advanced.accessibility.entrySize", manager.getConfig().getEntrySize() + amount * 0.075);
+ scaleIndicator.set();
+ } else if (favoritePanel.mouseScrolled(mouseX, mouseY, amount)) {
+ return true;
+ } else if (displayHistory.mouseScrolled(mouseX, mouseY, amount)) {
+ return true;
+ }
+ }
+ return super.mouseScrolled(mouseX, mouseY, amount);
+ }
+
+ @Override
+ public Rectangle getBounds() {
+ return fullBounds;
+ }
+
+ public EntryStacksRegionWidget<FavoriteEntry> getRegion() {
+ return region;
+ }
+
+ @Override
+ @Nullable
+ public DraggableComponent<Object> getHovered(DraggingContext<Screen> context, double mouseX, double mouseY) {
+ DraggableComponent<?> stack = region.getHoveredStack(context, mouseX, mouseY);
+ if (stack != null) return (DraggableComponent<Object>) stack;
+ if (favoritePanel.containsMouse(mouseX, mouseY)) {
+ stack = favoritePanel.getHoveredStack(mouseX, mouseY);
+ if (stack != null) return (DraggableComponent<Object>) stack;
+ }
+ stack = displayHistory.getHovered(context, mouseX, mouseY);
+ if (stack != null) return (DraggableComponent<Object>) stack;
+
+ return null;
+ }
+
+ @Override
+ public DraggedAcceptorResult acceptDragged(DraggingContext<Screen> context, DraggableComponent<?> stack) {
+ if (favoritePanel.containsMouse(context.getCurrentPosition()) || trash.containsMouse(context.getCurrentPosition())) {
+ context.renderToVoid(stack);
+ return DraggedAcceptorResult.CONSUMED;
+ }
+ return Stream.of(region, displayHistory)
+ .map(visitor -> visitor.acceptDragged(context, stack))
+ .filter(result -> result != DraggedAcceptorResult.PASS)
+ .findFirst()
+ .orElse(DraggedAcceptorResult.PASS);
+ }
+
+ @Override
+ public EntryStack<?> getFocusedStack() {
+ Point mouse = mouse();
+ EntryStack<?> stack = region.getFocusedStack();
+ if (stack != null && !stack.isEmpty()) return stack;
+ if (favoritePanel.containsMouse(mouse)) {
+ EntryStack<?> focusedStack = favoritePanel.getFocusedStack(mouse);
+
+ if (focusedStack != null) {
+ return focusedStack;
+ }
+ }
+ return EntryStack.empty();
+ }
+
+ @Override
+ public Stream<EntryStack<?>> getEntries() {
+ return region.getEntries();
+ }
+
+ @Override
+ public void render(PoseStack matrices, int mouseX, int mouseY, float delta) {
+ if (fullBounds.isEmpty())
+ return;
+
+ this.trash.render(matrices, mouseX, mouseY, delta);
+ double trashHeight = this.trash.getHeight();
+
+ boolean draggingDisplay = DraggingContext.getInstance().isDraggingComponent()
+ && DraggingContext.getInstance().getDragged().get() instanceof Display;
+ int topOffsetHeight = 0;
+ this.favoritesBounds = DisplayHistoryManager.INSTANCE.getEntries(displayHistory).isEmpty() && !draggingDisplay
+ ? fullBounds : RectangleUtils.excludeZones(this.fullBounds, Stream.of(displayHistory.createBounds(this.excludedBounds)));
+
+ displayHistory.render(matrices, mouseX, mouseY, delta);
+
+ if (favoritePanel.getBounds().height > 20) {
+ // Opened favorites panel
+ region.getBounds().setBounds(this.favoritesBounds.x, this.favoritesBounds.y + topOffsetHeight, this.favoritesBounds.width, this.favoritesBounds.height - topOffsetHeight - (this.favoritesBounds.getMaxY() - this.favoritePanel.getBounds().y) - 4 - (Math.round(trashHeight) <= 0 ? 0 : trashHeight));
+ } else {
+ region.getBounds().setBounds(this.favoritesBounds.x, this.favoritesBounds.y + topOffsetHeight, this.favoritesBounds.width, this.favoritesBounds.height - topOffsetHeight - (Math.round(trashHeight) <= 0 ? 0 : trashHeight + 24));
+ }
+
+ region.render(matrices, mouseX, mouseY, delta);
+ renderAddFavorite(matrices, mouseX, mouseY, delta);
+
+ this.scaleIndicator.setCenter(favoritesBounds.getCenterX(), favoritesBounds.getCenterY());
+ this.scaleIndicator.render(matrices, mouseX, mouseY, delta);
+ }
+
+ private void renderAddFavorite(PoseStack matrices, int mouseX, int mouseY, float delta) {
+ this.favoritePanel.render(matrices, mouseX, mouseY, delta);
+ this.togglePanelButton.render(matrices, mouseX, mouseY, delta);
+ }
+
+ @Override
+ public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
+ if (containsMouse(mouse()))
+ for (Widget widget : children())
+ if (widget.keyPressed(keyCode, scanCode, modifiers))
+ return true;
+ if (displayHistory.keyPressed(keyCode, scanCode, modifiers))
+ return true;
+ return false;
+ }
+
+ @Override
+ public void initBounds() {
+ this.fullBounds = REIRuntime.getInstance().calculateFavoritesListArea();
+ this.excludedBounds = RectangleUtils.excludeZones(this.fullBounds, ScreenRegistry.getInstance().exclusionZones().getExclusionZones(minecraft.screen).stream());
+ this.favoritesBounds = RectangleUtils.excludeZones(this.fullBounds, Stream.of(displayHistory.createBounds(this.fullBounds, null)));
+ this.updateSearch();
+ }
+
+ @Override
+ public void queueReloadSearch() {
+ updateSearch();
+ }
+
+ public void updateSearch() {
+ if (ConfigObject.getInstance().isFavoritesEnabled()) {
+ region.setEntries(CollectionUtils.map(ConfigObject.getInstance().getFavoriteEntries(), FavoriteEntry::copy), EntryStacksRegionWidget.RemovalMode.DISAPPEAR);
+ } else region.setEntries(Collections.emptyList(), EntryStacksRegionWidget.RemovalMode.DISAPPEAR);
+ }
+
+ @Override
+ public List<? extends Widget> children() {
+ return children;
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (region.mouseClicked(mouseX, mouseY, button))
+ return true;
+ for (Widget widget : children())
+ if (widget.mouseClicked(mouseX, mouseY, button))
+ return true;
+ if (displayHistory.mouseClicked(mouseX, mouseY, button))
+ return true;
+ return false;
+ }
+
+ @Override
+ public boolean mouseReleased(double mouseX, double mouseY, int button) {
+ if (containsMouse(mouseX, mouseY)) {
+ for (Widget widget : children())
+ if (widget.mouseReleased(mouseX, mouseY, button))
+ return true;
+ }
+ if (displayHistory.mouseReleased(mouseX, mouseY, button))
+ return true;
+ return false;
+ }
+
+ @Override
+ public Widget asWidget() {
+ return this;
+ }
+
+ @Override
+ public Rectangle getFavoritesBounds() {
+ return favoritesBounds;
+ }
+
+ @Override
+ public void submitDisplayHistory(Display display, @Nullable Rectangle fromBounds) {
+ displayHistory.addDisplay(fromBounds, display);
+ }
+}
diff --git a/runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/history/DisplayEntry.java b/runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/history/DisplayEntry.java
new file mode 100644
index 000000000..7af1d8a8b
--- /dev/null
+++ b/runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/history/DisplayEntry.java
@@ -0,0 +1,322 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020, 2021, 2022 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl.client.gui.widget.favorites.history;
+
+import com.mojang.blaze3d.vertex.PoseStack;
+import com.mojang.math.Vector4f;
+import me.shedaniel.clothconfig2.api.LazyResettable;
+import me.shedaniel.clothconfig2.api.animator.ValueAnimator;
+import me.shedaniel.math.Dimension;
+import me.shedaniel.math.FloatingRectangle;
+import me.shedaniel.math.Point;
+import me.shedaniel.math.Rectangle;
+import me.shedaniel.rei.api.client.gui.widgets.*;
+import me.shedaniel.rei.api.client.registry.category.CategoryRegistry;
+import me.shedaniel.rei.api.client.registry.display.DisplayCategory;
+import me.shedaniel.rei.api.client.view.ViewSearchBuilder;
+import me.shedaniel.rei.api.common.category.CategoryIdentifier;
+import me.shedaniel.rei.api.common.display.Display;
+import me.shedaniel.rei.impl.client.ClientInternals;
+import me.shedaniel.rei.impl.client.provider.AutoCraftingEvaluator;
+import net.minecraft.client.gui.components.events.GuiEventListener;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.network.chat.TextComponent;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.UUID;
+
+@SuppressWarnings("UnstableApiUsage")
+public class DisplayEntry extends WidgetWithBounds {
+ private final LazyResettable<List<Widget>> widgets = new LazyResettable<>(this::setupWidgets);
+ private final DisplayHistoryWidget parent;
+ private final Display display;
+ private final Dimension size = new Dimension(1, 1);
+ private boolean hasInitialBounds;
+ private final ValueAnimator<FloatingRectangle> bounds = ValueAnimator.ofFloatingRectangle();
+ private final Button plusButton;
+ private double xOffset = 0;
+ private boolean reachedStable = false;
+ private UUID uuid = UUID.randomUUID();
+
+ public DisplayEntry(DisplayHistoryWidget parent, Display display, @Nullable Rectangle initialBounds) {
+ this.display = display;
+ this.parent = parent;
+ this.hasInitialBounds = initialBounds != null;
+ if (this.hasInitialBounds) {
+ this.bounds.setAs(initialBounds.getFloatingBounds());
+ this.plusButton = Widgets.createButton(new Rectangle(initialBounds.getMaxX() - 16, initialBounds.getMaxY() - 16, 10, 10), new TextComponent("+"));
+ } else {
+ this.plusButton = Widgets.createButton(new Rectangle(-1000, -1000, 10, 10), new TextComponent("+"));
+ }
+ }
+
+ public UUID getUuid() {
+ return uuid;
+ }
+
+ public void setUuid(UUID uuid) {
+ this.uuid = uuid;
+ }
+
+ public void markBoundsDirty() {
+ widgets.reset();
+ }
+
+ private List<Widget> setupWidgets() {
+ Rectangle parentBounds = parent.getBounds();
+ CategoryRegistry.CategoryConfiguration<Display> configuration = CategoryRegistry.getInstance().get((CategoryIdentifier<Display>) display.getCategoryIdentifier());
+ DisplayCategory<Display> category = configuration.getCategory();
+ Rectangle displayBounds = new Rectangle(0, 0, category.getDisplayWidth(display), category.getDisplayHeight());
+ List<Widget> widgets = configuration.getView(display).setupDisplay(display, displayBounds);
+ float scale = 1.0F;
+ if (parentBounds.width * scale < displayBounds.width) {
+ scale = Math.min(scale, parentBounds.width * scale / (float) displayBounds.width);
+ }
+ if (parentBounds.height * scale < displayBounds.height) {
+ scale = Math.min(scale, parentBounds.height * scale / (float) displayBounds.height);
+ }
+ float x = parentBounds.getCenterX() - displayBounds.width / 2 * scale;
+ float y = parentBounds.getCenterY() - displayBounds.height / 2 * scale;
+ FloatingRectangle newBounds = new Rectangle(x, y, displayBounds.width * scale, displayBounds.height * scale).getFloatingBounds();
+ if (hasInitialBounds) {
+ if (this.size.width == 1 && this.size.height == 1) {
+ this.bounds.setTo(newBounds, 700);
+ } else {
+ this.bounds.setAs(newBounds);
+ }
+ } else {
+ this.bounds.setAs(newBounds);
+ hasInitialBounds = true;
+ }
+ this.size.setSize(displayBounds.getSize());
+ return widgets;
+ }
+
+ @Override
+ public Rectangle getBounds() {
+ return bounds.value().getBounds();
+ }
+
+ public Dimension getSize() {
+ return size;
+ }
+
+ public boolean isStable() {
+ widgets.get();
+ FloatingRectangle target = this.bounds.target();
+ FloatingRectangle value = this.bounds.value();
+ return reachedStable || Math.abs(value.x - target.x) <= 0.5 && Math.abs(value.y - target.y) <= 0.5 && Math.abs(value.width - target.width) <= 0.5 && Math.abs(value.height - target.height) <= 0.5;
+ }
+
+ public void setReachedStable(boolean reachedStable) {
+ this.reachedStable = reachedStable;
+ }
+
+ @Override
+ public void render(PoseStack poses, int mouseX, int mouseY, float delta) {
+ boolean stable = isStable();
+ this.bounds.update(delta);
+ FloatingRectangle target = this.bounds.target();
+ FloatingRectangle bounds = this.bounds.value();
+
+ if (!reachedStable && Math.abs(bounds.x - target.x) <= 0.5 && Math.abs(bounds.y - target.y) <= 0.5 && Math.abs(bounds.width - target.width) <= 0.5 && Math.abs(bounds.height - target.height) <= 0.5) {
+ reachedStable = true;
+ }
+
+ if (stable && (bounds.getMaxX() + xOffset < parent.getBounds().x || bounds.x + xOffset > parent.getBounds().getMaxX())) {
+ return;
+ }
+
+ poses.pushPose();
+ if (!stable || !target.equals(bounds)) {
+ poses.translate(0, 0, 600);
+ }
+ poses.translate(xOffset(), yOffset(), 0);
+ poses.scale(xScale(), yScale(), 1.0F);
+
+ for (Widget widget : widgets.get()) {
+ widget.render(poses, transformMouseX(mouseX), transformMouseY(mouseY), delta);
+ }
+ poses.popPose();
+
+ {
+ poses.pushPose();
+ if (stable && target.equals(bounds)) {
+ poses.translate(this.xOffset, 0, 200);
+ } else {
+ poses.translate(0, 0, 800);
+ }
+ Vector4f mouse = new Vector4f((float) mouseX, (float) mouseY, 0, 1);
+ mouse.transform(poses.last().pose());
+
+ AutoCraftingEvaluator.Result result = ClientInternals.getAutoCraftingEvaluator(display)
+ .buildRenderer()
+ .buildTooltipRenderer()
+ .get();
+
+ plusButton.setEnabled(result.isSuccessful());
+ plusButton.setTint(result.getTint());
+ plusButton.getBounds().setBounds(new Rectangle(bounds.getMaxX() - 14, bounds.getMaxY() - 14, 10, 10));
+
+ if (result.isApplicable()) {
+ plusButton.setText(new TextComponent("+"));
+ plusButton.render(poses, Math.round(mouse.x()), Math.round(mouse.y()), delta);
+ poses.popPose();
+
+ if (plusButton.containsMouse(Math.round(mouse.x()), Math.round(mouse.y())) && result.getTooltipRenderer() != null) {
+ result.getTooltipRenderer().accept(new Point(mouseX, mouseY), Tooltip::queue);
+ }
+
+ if (result.getRenderer() != null) {
+ poses.pushPose();
+ if (!stable || !target.equals(bounds)) {
+ poses.translate(0, 0, 600);
+ }
+ poses.translate(xOffset(), yOffset(), 0);
+ poses.scale(xScale(), yScale(), 1.0F);
+
+ result.getRenderer().render(poses, mouseX, mouseY, delta, widgets.get(), getBounds(), display);
+ poses.popPose();
+ }
+ } else {
+ poses.popPose();
+ }
+ }
+ }
+
+ @Override
+ public boolean mouseClicked(double mouseX, double mouseY, int button) {
+ if (containsMouse(mouseX + xOffset, mouseY)) {
+ for (Widget widget : widgets.get()) {
+ if (widget.mouseClicked(transformMouseX(mouseX), transformMouseY(mouseY), button)) {
+ return true;
+ }
+ }
+ }
+
+ return super.mouseClicked(mouseX, mouseY, button);
+ }
+
+ @Override
+ public boolean mouseReleased(double mouseX, double mouseY, int button) {
+ if (containsMouse(mouseX + xOffset, mouseY)) {
+ for (Widget widget : widgets.get()) {
+ if (widget.mouseReleased(transformMouseX(mouseX), transformMouseY(mouseY), button)) {
+ return true;
+ }
+ }
+
+ if (button == 0 && plusButton.containsMouse(mouseX + xOffset, mouseY)) {
+ ClientInternals.getAutoCraftingEvaluator(display)
+ .actuallyCraft()
+ .stacked(Screen.hasShiftDown())
+ .get();
+ Widgets.produceClickSound();
+ return true;
+ }
+
+ ViewSearchBuilder.builder().addDisplays(List.of(display)).open();
+ Widgets.produceClickSound();
+ return true;
+ }
+
+ return super.mouseReleased(mouseX, mouseY, button);
+ }
+
+ @Override
+ public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
+ try {
+ Widget.pushMouse(new Point(transformMouseX(mouse().x), transformMouseY(mouse().y)));
+ for (Widget widget : widgets.get()) {
+ if (widget.keyPressed(keyCode, scanCode, modifiers)) {
+ return true;
+ }
+ }
+ } finally {
+ Widget.popMouse();
+ }
+
+ return super.keyPressed(keyCode, scanCode, modifiers);
+ }
+
+ private float xOffset() {
+ FloatingRectangle bounds = this.bounds.value();
+ FloatingRectangle target = this.bounds.target();
+ float xOffset = (float) bounds.x;
+ if (isStable() && target.equals(bounds)) {
+ xOffset += this.xOffset;
+ }
+ return xOffset;
+ }
+
+ private float yOffset() {
+ FloatingRectangle bounds = this.bounds.value();
+ return (float) bounds.y;
+ }
+
+ private float xScale() {
+ FloatingRectangle bounds = this.bounds.value();
+ return (float) bounds.width / size.width;
+ }
+
+ private float yScale() {
+ FloatingRectangle bounds = this.bounds.value();
+ return (float) bounds.height / size.height;
+ }
+
+ protected int transformMouseX(int mouseX) {
+ return Math.round((mouseX - xOffset()) / xScale());
+ }
+
+ protected int transformMouseY(int mouseY) {
+ return Math.round((mouseY - yOffset()) / yScale());
+ }
+
+ protected double transformMouseX(double mouseX) {
+ return (mouseX - xOffset()) / xScale();
+ }
+
+ protected double transformMouseY(double mouseY) {
+ return (mouseY - yOffset()) / yScale();
+ }
+
+ @Override
+ public List<? extends GuiEventListener> children() {
+ return Collections.emptyList();
+ }
+
+ public void setScrolled(double xOffset) {
+ this.xOffset = xOffset;
+ }
+
+ public List<Widget> getWidgets() {
+ return widgets.get();
+ }
+
+ public Display getDisplay() {
+ return display;
+ }
+}
diff --git a/runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/history/DisplayHistoryManager.java b/runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/history/DisplayHistoryManager.java
new file mode 100644
index 000000000..d115190ff
--- /dev/null
+++ b/runtime-frontend/favorites-entries/src/main/java/me/shedaniel/rei/impl/client/gui/widget/favorites/history/DisplayHistoryManager.java
@@ -0,0 +1,147 @@
+/*
+ * This file is licensed under the MIT License, part of Roughly Enough Items.
+ * Copyright (c) 2018, 2019, 2020, 2021, 2022 shedaniel
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.shedaniel.rei.impl.client.gui.widget.favorites.history;
+
+import com.google.common.collect.Iterables;
+import me.shedaniel.math.Rectangle;
+import me.shedaniel.rei.api.client.config.ConfigManager;
+import me.shedaniel.rei.api.client.registry.category.CategoryRegistry;
+import me.shedaniel.rei.api.common.category.CategoryIdentifier;
+import me.shedaniel.rei.api.common.display.Display;
+import me.shedaniel.rei.api.common.display.DisplaySerializerRegistry;
+import me.shedaniel.rei.api.common.plugins.PluginManager;
+import me.shedaniel.rei.impl.client.config.ConfigManagerInternal;
+import me.shedaniel.rei.impl.common.InternalLogger;
+import net.minecraft.Util;
+import net.minecraft.nbt.CompoundTag;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.*;
+
+public class DisplayHistoryManager {
+ public static final DisplayHistoryManager INSTANCE = new DisplayHistoryManager();
+ private Map<String, DisplayEntry> entries = new LinkedHashMap<>();
+ private long lastCheckTime = -1;
+
+ private List<CompoundTag> getDisplayHistory() {
+ return (List<CompoundTag>) ConfigManagerInternal.getInstance().get("basics.displayHistory");
+ }
+
+ public Collection<DisplayEntry> getEntries(DisplayHistoryWidget parent) {
+ if ((lastCheckTime == -1 || Util.getMillis() - lastCheckTime > 4000) && !PluginManager.areAnyReloading()) {
+ updateEntries(parent);
+ lastCheckTime = Util.getMillis();
+ }
+
+ return Collections.unmodifiableCollection(entries.values());
+ }
+
+ private void update