From 55d087841b419276087fa79fafe11100cefbf6ec Mon Sep 17 00:00:00 2001
From: raoulvdberge <raoulvdberge@gmail.com>
Date: Sat, 1 Apr 2023 14:08:31 +0200
Subject: [PATCH] feat: the detector now is a proper amount screen

---
 CHANGELOG.md                                  |   5 +
 .../detector/DetectorContainerMenu.java       |   4 +-
 .../common/screen/DetectorScreen.java         | 101 ++++---------
 .../screen/amount/AbstractAmountScreen.java   | 142 ++++++++++--------
 .../screen/amount/AmountOperations.java       |  14 ++
 .../amount/AmountScreenConfiguration.java     | 134 +++++++++++++----
 .../screen/amount/DoubleAmountOperations.java |  61 ++++++++
 .../amount/IntegerAmountOperations.java       |  49 ++++++
 .../screen/amount/LongAmountOperations.java   |  49 ++++++
 .../common/screen/amount/PriorityScreen.java  |  10 +-
 .../screen/amount/ResourceAmountScreen.java   |  24 +--
 .../refinedstorage2/textures/gui/detector.png | Bin 806 -> 815 bytes
 12 files changed, 412 insertions(+), 181 deletions(-)
 create mode 100644 refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/AmountOperations.java
 create mode 100644 refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/DoubleAmountOperations.java
 create mode 100644 refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/IntegerAmountOperations.java
 create mode 100644 refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/LongAmountOperations.java

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 60fbd74da..0d692722e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,11 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
 
+### Changed
+
+-   The Detector screen now is a proper amount screen by having increment/decrement buttons and scrollbar support.
+-   The amount in an amount screen is now colored red if the amount is invalid.
+
 ## [2.0.0-milestone.2.9] - 2023-03-31
 
 ### Fixed
diff --git a/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/containermenu/detector/DetectorContainerMenu.java b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/containermenu/detector/DetectorContainerMenu.java
index dce807147..1ce391937 100644
--- a/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/containermenu/detector/DetectorContainerMenu.java
+++ b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/containermenu/detector/DetectorContainerMenu.java
@@ -56,8 +56,8 @@ public DetectorContainerMenu(final int syncId,
     }
 
     private void addSlots(final ResourceFilterContainer config) {
-        addSlot(new ResourceFilterSlot(config, 0, 107, 20));
-        addPlayerInventory(player.getInventory(), 8, 55);
+        addSlot(new ResourceFilterSlot(config, 0, 116, 47));
+        addPlayerInventory(player.getInventory(), 8, 106);
         transferManager.addFilterTransfer(player.getInventory());
     }
 
diff --git a/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/DetectorScreen.java b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/DetectorScreen.java
index d72cce0c3..d4fc0bd7a 100644
--- a/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/DetectorScreen.java
+++ b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/DetectorScreen.java
@@ -2,78 +2,51 @@
 
 import com.refinedmods.refinedstorage2.platform.common.containermenu.detector.DetectorContainerMenu;
 import com.refinedmods.refinedstorage2.platform.common.containermenu.property.PropertyTypes;
+import com.refinedmods.refinedstorage2.platform.common.screen.amount.AbstractAmountScreen;
+import com.refinedmods.refinedstorage2.platform.common.screen.amount.AmountScreenConfiguration;
+import com.refinedmods.refinedstorage2.platform.common.screen.amount.DoubleAmountOperations;
 import com.refinedmods.refinedstorage2.platform.common.screen.widget.DetectorModeSideButtonWidget;
 import com.refinedmods.refinedstorage2.platform.common.screen.widget.FuzzyModeSideButtonWidget;
 
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
-import java.util.Locale;
-import java.util.function.Predicate;
-import java.util.regex.Pattern;
-import javax.annotation.Nullable;
-
 import com.mojang.blaze3d.vertex.PoseStack;
-import net.minecraft.client.gui.components.EditBox;
 import net.minecraft.network.chat.Component;
 import net.minecraft.resources.ResourceLocation;
 import net.minecraft.world.entity.player.Inventory;
+import org.joml.Vector3f;
 
 import static com.refinedmods.refinedstorage2.platform.common.util.IdentifierUtil.createIdentifier;
 
-public class DetectorScreen extends AbstractBaseScreen<DetectorContainerMenu> {
+public class DetectorScreen extends AbstractAmountScreen<DetectorContainerMenu, Double> {
     private static final ResourceLocation TEXTURE = createIdentifier("textures/gui/detector.png");
-    private static final Predicate<String> DECIMAL_PATTERN = Pattern.compile("\\d*\\.?\\d*").asMatchPredicate();
-    private static final DecimalFormat DECIMAL_FORMAT;
-
-    static {
-        final DecimalFormatSymbols initialAmountSymbols = new DecimalFormatSymbols(Locale.ROOT);
-        initialAmountSymbols.setDecimalSeparator('.');
-        DECIMAL_FORMAT = new DecimalFormat("##.###", initialAmountSymbols);
-        DECIMAL_FORMAT.setGroupingUsed(false);
-    }
-
-    @Nullable
-    private EditBox amountBox;
 
     public DetectorScreen(final DetectorContainerMenu menu, final Inventory playerInventory, final Component text) {
-        super(menu, playerInventory, text);
-        this.inventoryLabelY = 43;
+        super(
+            menu,
+            null,
+            playerInventory,
+            text,
+            AmountScreenConfiguration.AmountScreenConfigurationBuilder.<Double>create()
+                .withInitialAmount(menu.getAmount())
+                .withIncrementsTop(1, 10, 64)
+                .withIncrementsBottom(-1, -10, -64)
+                .withIncrementsTopStartPosition(new Vector3f(40, 20, 0))
+                .withIncrementsBottomStartPosition(new Vector3f(40, 70, 0))
+                .withAmountFieldWidth(59)
+                .withAmountFieldPosition(new Vector3f(45, 51, 0))
+                .withActionButtonsEnabled(false)
+                .withMinAmount(0D)
+                .withResetAmount(0D)
+                .build(),
+            DoubleAmountOperations.INSTANCE
+        );
+        this.inventoryLabelY = 94;
         this.imageWidth = 176;
-        this.imageHeight = 137;
+        this.imageHeight = 188;
     }
 
     @Override
     protected void init() {
         super.init();
-        if (amountBox == null) {
-            amountBox = new EditBox(
-                font,
-                leftPos + 41,
-                topPos + 24,
-                50,
-                font.lineHeight,
-                Component.literal("")
-            );
-        } else {
-            amountBox.setX(leftPos + 41);
-            amountBox.setY(topPos + 24);
-        }
-        amountBox.setFocus(false);
-        amountBox.setCanLoseFocus(true);
-        amountBox.setBordered(false);
-        amountBox.setFilter(DECIMAL_PATTERN);
-        amountBox.setValue(DECIMAL_FORMAT.format(menu.getAmount()));
-        amountBox.setResponder(value -> {
-            try {
-                final double amount = value.trim().isEmpty()
-                    ? 0
-                    : Double.parseDouble(value);
-                menu.changeAmountOnClient(amount);
-            } catch (final NumberFormatException e) {
-                // do nothing
-            }
-        });
-        addWidget(amountBox);
         addSideButton(new FuzzyModeSideButtonWidget(
             getMenu().getProperty(PropertyTypes.FUZZY_MODE),
             this::renderComponentTooltip
@@ -85,26 +58,18 @@ protected void init() {
     }
 
     @Override
-    public void render(final PoseStack poseStack, final int mouseX, final int mouseY, final float partialTicks) {
-        super.render(poseStack, mouseX, mouseY, partialTicks);
-        if (amountBox != null) {
-            amountBox.render(poseStack, mouseX, mouseY, partialTicks);
-        }
+    protected void accept(final Double amount) {
+        getMenu().changeAmountOnClient(amount);
     }
 
     @Override
-    public boolean charTyped(final char unknown1, final int unknown2) {
-        return (amountBox != null && amountBox.charTyped(unknown1, unknown2)) || super.charTyped(unknown1, unknown2);
-    }
-
-    @Override
-    public boolean keyPressed(final int key, final int scanCode, final int modifiers) {
-        return (amountBox != null && amountBox.keyPressed(key, scanCode, modifiers))
-            || super.keyPressed(key, scanCode, modifiers);
+    protected ResourceLocation getTexture() {
+        return TEXTURE;
     }
 
     @Override
-    protected ResourceLocation getTexture() {
-        return TEXTURE;
+    protected void renderLabels(final PoseStack poseStack, final int mouseX, final int mouseY) {
+        super.renderLabels(poseStack, mouseX, mouseY);
+        font.draw(poseStack, this.playerInventoryTitle, inventoryLabelX, inventoryLabelY, 4210752);
     }
 }
diff --git a/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/AbstractAmountScreen.java b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/AbstractAmountScreen.java
index 5f8e2a239..67f9d28f1 100644
--- a/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/AbstractAmountScreen.java
+++ b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/AbstractAmountScreen.java
@@ -2,17 +2,18 @@
 
 import com.refinedmods.refinedstorage2.platform.common.screen.AbstractBaseScreen;
 
+import java.util.Objects;
 import java.util.Optional;
 import javax.annotation.Nullable;
 
 import com.mojang.blaze3d.vertex.PoseStack;
+import net.minecraft.ChatFormatting;
 import net.minecraft.client.Minecraft;
 import net.minecraft.client.gui.components.Button;
 import net.minecraft.client.gui.components.EditBox;
 import net.minecraft.client.gui.screens.Screen;
 import net.minecraft.network.chat.Component;
 import net.minecraft.network.chat.MutableComponent;
-import net.minecraft.util.Mth;
 import net.minecraft.world.entity.player.Inventory;
 import net.minecraft.world.entity.player.Player;
 import net.minecraft.world.inventory.AbstractContainerMenu;
@@ -22,58 +23,45 @@
 
 import static com.refinedmods.refinedstorage2.platform.common.util.IdentifierUtil.createTranslation;
 
-public abstract class AbstractAmountScreen extends AbstractBaseScreen<AbstractContainerMenu> {
+public abstract class AbstractAmountScreen<T extends AbstractContainerMenu, N extends Number>
+    extends AbstractBaseScreen<T> {
     private static final MutableComponent SET_TEXT = createTranslation("gui", "amount.set");
     private static final MutableComponent RESET_TEXT = createTranslation("gui", "amount.reset");
     private static final MutableComponent CANCEL_TEXT = Component.translatable("gui.cancel");
 
     private static final int INCREMENT_BUTTON_WIDTH = 30;
-    private static final int INCREMENT_BUTTON_X = 7;
-    private static final int INCREMENT_BUTTON_TOP_Y = 20;
-
     private static final int ACTION_BUTTON_WIDTH = 50;
 
+    @Nullable
     private final Screen parent;
+    private final AmountScreenConfiguration<N> configuration;
+    private final AmountOperations<N> amountOperations;
 
     @Nullable
     private EditBox amountField;
     @Nullable
     private Button confirmButton;
 
-    private final AmountScreenConfiguration configuration;
-
-    protected AbstractAmountScreen(final Screen parent,
-                                   final Inventory playerInventory,
-                                   final Component title,
-                                   final AmountScreenConfiguration configuration) {
-        this(new DefaultDummyContainerMenu(), parent, playerInventory, title, configuration);
-    }
-
-    protected AbstractAmountScreen(final AbstractContainerMenu containerMenu,
-                                   final Screen parent,
+    protected AbstractAmountScreen(final T containerMenu,
+                                   @Nullable final Screen parent,
                                    final Inventory playerInventory,
                                    final Component title,
-                                   final AmountScreenConfiguration configuration) {
+                                   final AmountScreenConfiguration<N> configuration,
+                                   final AmountOperations<N> amountOperations) {
         super(containerMenu, playerInventory, title);
         this.parent = parent;
         this.configuration = configuration;
+        this.amountOperations = amountOperations;
     }
 
     @Override
     protected void init() {
         super.init();
-        addActionButtons();
+        if (configuration.isActionButtonsEnabled()) {
+            addActionButtons();
+        }
         addAmountField();
-        addIncrementButtons(
-            configuration.getIncrementsTop(),
-            leftPos + INCREMENT_BUTTON_X,
-            topPos + INCREMENT_BUTTON_TOP_Y
-        );
-        addIncrementButtons(
-            configuration.getIncrementsBottom(),
-            leftPos + INCREMENT_BUTTON_X,
-            topPos + imageHeight - 27
-        );
+        addIncrementButtons();
     }
 
     private void addActionButtons() {
@@ -83,11 +71,11 @@ private void addActionButtons() {
             .pos(leftPos + (int) pos.x(), topPos + (int) pos.y())
             .size(ACTION_BUTTON_WIDTH, 20)
             .build());
-        confirmButton = addRenderableWidget(Button.builder(SET_TEXT, btn -> tryConfirm())
+        confirmButton = addRenderableWidget(Button.builder(SET_TEXT, btn -> tryConfirmAndCloseToParent())
             .pos(leftPos + (int) pos.x(), topPos + (int) pos.y() + 24)
             .size(ACTION_BUTTON_WIDTH, 20)
             .build());
-        addRenderableWidget(Button.builder(CANCEL_TEXT, btn -> close())
+        addRenderableWidget(Button.builder(CANCEL_TEXT, btn -> tryCloseToParent())
             .pos(leftPos + (int) pos.x(), topPos + (int) pos.y() + 48)
             .size(ACTION_BUTTON_WIDTH, 20)
             .build());
@@ -95,30 +83,53 @@ private void addActionButtons() {
 
     private void addAmountField() {
         final Vector3f pos = configuration.getAmountFieldPosition();
-
         amountField = new EditBox(
             font,
             leftPos + (int) pos.x(),
             topPos + (int) pos.y(),
-            69 - 6,
+            configuration.getAmountFieldWidth() - 6,
             font.lineHeight,
             Component.empty()
         );
         amountField.setBordered(false);
-        amountField.setValue(String.valueOf(configuration.getInitialAmount()));
+        if (configuration.getInitialAmount() != null) {
+            amountField.setValue(amountOperations.format(configuration.getInitialAmount()));
+        }
         amountField.setVisible(true);
         amountField.setCanLoseFocus(false);
         amountField.setFocus(true);
         amountField.setResponder(value -> {
+            final boolean valid = getAndValidateAmount().isPresent();
             if (confirmButton != null) {
-                confirmButton.active = getAndValidateAmount().isPresent();
+                confirmButton.active = valid;
+            } else {
+                tryConfirm();
             }
+            amountField.setTextColor(valid
+                ? Objects.requireNonNullElse(ChatFormatting.WHITE.getColor(), 15)
+                : Objects.requireNonNullElse(ChatFormatting.RED.getColor(), 15)
+            );
         });
+        amountField.setTextColor(Objects.requireNonNullElse(ChatFormatting.WHITE.getColor(), 15));
+        setFocused(amountField);
 
         addRenderableWidget(amountField);
     }
 
-    protected abstract void accept(int amount);
+    private void addIncrementButtons() {
+        final Vector3f incrementsTopPos = configuration.getIncrementsTopStartPosition();
+        addIncrementButtons(
+            configuration.getIncrementsTop(),
+            leftPos + (int) incrementsTopPos.x,
+            topPos + (int) incrementsTopPos.y
+        );
+        final Vector3f incrementsBottomPos = configuration.getIncrementsBottomStartPosition();
+        addIncrementButtons(
+            configuration.getIncrementsBottom(),
+            leftPos + (int) incrementsBottomPos.x,
+            topPos + (int) incrementsBottomPos.y
+        );
+    }
 
     private void addIncrementButtons(final int[] increments, final int x, final int y) {
         for (int i = 0; i < increments.length; ++i) {
@@ -128,6 +139,8 @@ private void addIncrementButtons(final int[] increments, final int x, final int
         }
     }
 
+    protected abstract void accept(N amount);
+
     private Button createIncrementButton(final int x, final int y, final int increment) {
         final Component text = Component.literal((increment > 0 ? "+" : "") + increment);
         return Button.builder(text, btn -> changeAmount(increment))
@@ -141,13 +154,13 @@ private void changeAmount(final int delta) {
             return;
         }
         getAndValidateAmount().ifPresent(oldAmount -> {
-            final int newAmount = oldAmount + delta;
-            final int correctedNewAmount = Mth.clamp(
-                newAmount,
+            final N newAmount = amountOperations.changeAmount(
+                oldAmount,
+                delta,
                 configuration.getMinAmount(),
                 configuration.getMaxAmount()
             );
-            amountField.setValue(String.valueOf(correctedNewAmount));
+            amountField.setValue(amountOperations.format(newAmount));
         });
     }
 
@@ -175,13 +188,15 @@ public boolean charTyped(final char unknown1, final int unknown2) {
     @Override
     public boolean keyPressed(final int key, final int scanCode, final int modifiers) {
         if (key == GLFW.GLFW_KEY_ESCAPE) {
-            close();
+            if (!tryCloseToParent()) {
+                onClose();
+            }
             return true;
         }
         if (amountField != null
             && (key == GLFW.GLFW_KEY_ENTER || key == GLFW.GLFW_KEY_KP_ENTER)
             && amountField.isFocused()) {
-            tryConfirm();
+            tryConfirmAndCloseToParent();
             return true;
         }
         if (amountField != null
@@ -192,46 +207,43 @@ public boolean keyPressed(final int key, final int scanCode, final int modifiers
     }
 
     private void reset() {
-        if (amountField == null) {
+        if (amountField == null || configuration.getResetAmount() == null) {
             return;
         }
-        amountField.setValue(String.valueOf(configuration.getResetAmount()));
+        amountField.setValue(amountOperations.format(configuration.getResetAmount()));
     }
 
     private void tryConfirm() {
-        getAndValidateAmount().ifPresent(this::confirm);
-    }
-
-    private void confirm(final int amount) {
-        accept(amount);
-        close();
+        getAndValidateAmount().ifPresent(this::accept);
     }
 
-    private void close() {
-        Minecraft.getInstance().setScreen(parent);
+    private void tryConfirmAndCloseToParent() {
+        getAndValidateAmount().ifPresent(value -> {
+            accept(value);
+            tryCloseToParent();
+        });
     }
 
-    private Optional<Integer> getAndValidateAmount() {
-        if (amountField == null) {
-            return Optional.empty();
-        }
-        try {
-            final int amount = Integer.parseInt(amountField.getValue());
-            return validateAmount(amount);
-        } catch (NumberFormatException e) {
-            return Optional.empty();
+    private boolean tryCloseToParent() {
+        if (parent != null) {
+            Minecraft.getInstance().setScreen(parent);
+            return true;
         }
+        return false;
     }
 
-    private Optional<Integer> validateAmount(final int amount) {
-        if (amount >= configuration.getMinAmount() && amount <= configuration.getMaxAmount()) {
-            return Optional.of(amount);
-        } else {
+    private Optional<N> getAndValidateAmount() {
+        if (amountField == null) {
             return Optional.empty();
         }
+        return amountOperations.parse(amountField.getValue()).flatMap(amount -> amountOperations.validate(
+            amount,
+            configuration.getMinAmount(),
+            configuration.getMaxAmount()
+        ));
     }
 
-    private static class DefaultDummyContainerMenu extends AbstractContainerMenu {
+    protected static class DefaultDummyContainerMenu extends AbstractContainerMenu {
         protected DefaultDummyContainerMenu() {
             super(null, 0);
         }
diff --git a/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/AmountOperations.java b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/AmountOperations.java
new file mode 100644
index 000000000..0bad40ccf
--- /dev/null
+++ b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/AmountOperations.java
@@ -0,0 +1,14 @@
+package com.refinedmods.refinedstorage2.platform.common.screen.amount;
+
+import java.util.Optional;
+import javax.annotation.Nullable;
+
+public interface AmountOperations<N extends Number> {
+    String format(N value);
+
+    Optional<N> parse(String value);
+
+    Optional<N> validate(N amount, @Nullable N minAmount, @Nullable N maxAmount);
+
+    N changeAmount(N current, int delta, @Nullable N minAmount, @Nullable N maxAmount);
+}
diff --git a/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/AmountScreenConfiguration.java b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/AmountScreenConfiguration.java
index 3c72013d3..92fa4da4c 100644
--- a/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/AmountScreenConfiguration.java
+++ b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/AmountScreenConfiguration.java
@@ -1,36 +1,55 @@
 package com.refinedmods.refinedstorage2.platform.common.screen.amount;
 
+import javax.annotation.Nullable;
+
 import org.joml.Vector3f;
 
-public final class AmountScreenConfiguration {
-    private final int initialAmount;
+public final class AmountScreenConfiguration<T extends Number> {
+    @Nullable
+    private final T initialAmount;
     private final int[] incrementsTop;
+    private final Vector3f incrementsTopStartPosition;
     private final int[] incrementsBottom;
+    private final Vector3f incrementsBottomStartPosition;
+    private final int amountFieldWidth;
     private final Vector3f amountFieldPosition;
     private final Vector3f actionButtonsStartPosition;
-    private final int minAmount;
-    private final int maxAmount;
-    private final int resetAmount;
-
-    private AmountScreenConfiguration(final int initialAmount,
+    private final boolean actionButtonsEnabled;
+    @Nullable
+    private final T minAmount;
+    @Nullable
+    private final T maxAmount;
+    @Nullable
+    private final T resetAmount;
+
+    private AmountScreenConfiguration(@Nullable final T initialAmount,
                                       final int[] incrementsTop,
+                                      final Vector3f incrementsTopStartPosition,
                                       final int[] incrementsBottom,
+                                      final Vector3f incrementsBottomStartPosition,
+                                      final int amountFieldWidth,
                                       final Vector3f amountFieldPosition,
                                       final Vector3f actionButtonsStartPosition,
-                                      final int minAmount,
-                                      final int maxAmount,
-                                      final int resetAmount) {
+                                      final boolean actionButtonsEnabled,
+                                      @Nullable final T minAmount,
+                                      @Nullable final T maxAmount,
+                                      @Nullable final T resetAmount) {
         this.initialAmount = initialAmount;
         this.incrementsTop = incrementsTop;
+        this.incrementsTopStartPosition = incrementsTopStartPosition;
         this.incrementsBottom = incrementsBottom;
+        this.incrementsBottomStartPosition = incrementsBottomStartPosition;
+        this.amountFieldWidth = amountFieldWidth;
         this.amountFieldPosition = amountFieldPosition;
         this.actionButtonsStartPosition = actionButtonsStartPosition;
+        this.actionButtonsEnabled = actionButtonsEnabled;
         this.minAmount = minAmount;
         this.maxAmount = maxAmount;
         this.resetAmount = resetAmount;
     }
 
-    public int getInitialAmount() {
+    @Nullable
+    public T getInitialAmount() {
         return initialAmount;
     }
 
@@ -38,10 +57,22 @@ public int[] getIncrementsTop() {
         return incrementsTop;
     }
 
+    public Vector3f getIncrementsTopStartPosition() {
+        return incrementsTopStartPosition;
+    }
+
     public int[] getIncrementsBottom() {
         return incrementsBottom;
     }
 
+    public Vector3f getIncrementsBottomStartPosition() {
+        return incrementsBottomStartPosition;
+    }
+
+    public int getAmountFieldWidth() {
+        return amountFieldWidth;
+    }
+
     public Vector3f getAmountFieldPosition() {
         return amountFieldPosition;
     }
@@ -50,84 +81,125 @@ public Vector3f getActionButtonsStartPosition() {
         return actionButtonsStartPosition;
     }
 
-    public int getMinAmount() {
+    public boolean isActionButtonsEnabled() {
+        return actionButtonsEnabled;
+    }
+
+    @Nullable
+    public T getMinAmount() {
         return minAmount;
     }
 
-    public int getMaxAmount() {
+    @Nullable
+    public T getMaxAmount() {
         return maxAmount;
     }
 
-    public int getResetAmount() {
+    @Nullable
+    public T getResetAmount() {
         return resetAmount;
     }
 
-    public static final class AmountScreenConfigurationBuilder {
-        private int initialAmount;
+    public static final class AmountScreenConfigurationBuilder<T extends Number> {
+        @Nullable
+        private T initialAmount;
         private int[] incrementsTop = new int[] {};
+        private Vector3f incrementsTopStartPosition = new Vector3f(7, 20, 0);
         private int[] incrementsBottom = new int[] {};
+        private Vector3f incrementsBottomStartPosition = new Vector3f(7, 20 + 47, 0);
+        private int amountFieldWidth = 68;
         private Vector3f amountFieldPosition = new Vector3f(0, 0, 0);
         private Vector3f actionButtonsStartPosition = new Vector3f(0, 0, 0);
-        private int minAmount;
-        private int maxAmount;
-        private int resetAmount;
+        private boolean actionButtonsEnabled = true;
+        @Nullable
+        private T minAmount;
+        @Nullable
+        private T maxAmount;
+        @Nullable
+        private T resetAmount;
 
         private AmountScreenConfigurationBuilder() {
         }
 
-        public static AmountScreenConfigurationBuilder create() {
-            return new AmountScreenConfigurationBuilder();
+        public static <T extends Number> AmountScreenConfigurationBuilder<T> create() {
+            return new AmountScreenConfigurationBuilder<>();
         }
 
-        public AmountScreenConfigurationBuilder withInitialAmount(final int newInitialAmount) {
+        public AmountScreenConfigurationBuilder<T> withInitialAmount(final T newInitialAmount) {
             this.initialAmount = newInitialAmount;
             return this;
         }
 
-        public AmountScreenConfigurationBuilder withIncrementsTop(final int... newIncrementsTop) {
+        public AmountScreenConfigurationBuilder<T> withIncrementsTop(final int... newIncrementsTop) {
             this.incrementsTop = newIncrementsTop;
             return this;
         }
 
-        public AmountScreenConfigurationBuilder withIncrementsBottom(final int... newIncrementsBottom) {
+        public AmountScreenConfigurationBuilder<T> withIncrementsTopStartPosition(final Vector3f newPos) {
+            this.incrementsTopStartPosition = newPos;
+            return this;
+        }
+
+        public AmountScreenConfigurationBuilder<T> withIncrementsBottom(final int... newIncrementsBottom) {
             this.incrementsBottom = newIncrementsBottom;
             return this;
         }
 
-        public AmountScreenConfigurationBuilder withAmountFieldPosition(final Vector3f newAmountFieldPosition) {
+        public AmountScreenConfigurationBuilder<T> withIncrementsBottomStartPosition(final Vector3f newPos) {
+            this.incrementsBottomStartPosition = newPos;
+            return this;
+        }
+
+        public AmountScreenConfigurationBuilder<T> withAmountFieldWidth(final int newAmountFieldWidth) {
+            this.amountFieldWidth = newAmountFieldWidth;
+            return this;
+        }
+
+        public AmountScreenConfigurationBuilder<T> withAmountFieldPosition(final Vector3f newAmountFieldPosition) {
             this.amountFieldPosition = newAmountFieldPosition;
             return this;
         }
 
-        public AmountScreenConfigurationBuilder withActionButtonsStartPosition(
+        public AmountScreenConfigurationBuilder<T> withActionButtonsStartPosition(
             final Vector3f newActionButtonsStartPosition
         ) {
             this.actionButtonsStartPosition = newActionButtonsStartPosition;
             return this;
         }
 
-        public AmountScreenConfigurationBuilder withMinAmount(final int newMinAmount) {
+        public AmountScreenConfigurationBuilder<T> withActionButtonsEnabled(
+            final boolean newActionButtonsEnabled
+        ) {
+            this.actionButtonsEnabled = newActionButtonsEnabled;
+            return this;
+        }
+
+        public AmountScreenConfigurationBuilder<T> withMinAmount(final T newMinAmount) {
             this.minAmount = newMinAmount;
             return this;
         }
 
-        public AmountScreenConfigurationBuilder withMaxAmount(final int newMaxAmount) {
+        public AmountScreenConfigurationBuilder<T> withMaxAmount(final T newMaxAmount) {
             this.maxAmount = newMaxAmount;
             return this;
         }
 
-        public AmountScreenConfigurationBuilder withResetAmount(final int newResetAmount) {
+        public AmountScreenConfigurationBuilder<T> withResetAmount(final T newResetAmount) {
             this.resetAmount = newResetAmount;
             return this;
         }
 
-        public AmountScreenConfiguration build() {
-            return new AmountScreenConfiguration(
+        public AmountScreenConfiguration<T> build() {
+            return new AmountScreenConfiguration<T>(
                 initialAmount,
                 incrementsTop,
+                incrementsTopStartPosition,
                 incrementsBottom,
+                incrementsBottomStartPosition,
+                amountFieldWidth,
                 amountFieldPosition,
                 actionButtonsStartPosition,
+                actionButtonsEnabled,
                 minAmount,
                 maxAmount,
                 resetAmount
diff --git a/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/DoubleAmountOperations.java b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/DoubleAmountOperations.java
new file mode 100644
index 000000000..962f8185a
--- /dev/null
+++ b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/DoubleAmountOperations.java
@@ -0,0 +1,61 @@
+package com.refinedmods.refinedstorage2.platform.common.screen.amount;
+
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Optional;
+import javax.annotation.Nullable;
+
+import net.minecraft.util.Mth;
+
+public class DoubleAmountOperations implements AmountOperations<Double> {
+    public static final AmountOperations<Double> INSTANCE = new DoubleAmountOperations();
+
+    private static final DecimalFormat DECIMAL_FORMAT;
+
+    static {
+        final DecimalFormatSymbols initialAmountSymbols = new DecimalFormatSymbols(Locale.ROOT);
+        initialAmountSymbols.setDecimalSeparator('.');
+        DECIMAL_FORMAT = new DecimalFormat("##.###", initialAmountSymbols);
+        DECIMAL_FORMAT.setGroupingUsed(false);
+    }
+
+    private DoubleAmountOperations() {
+    }
+
+    @Override
+    public String format(final Double value) {
+        return DECIMAL_FORMAT.format(value);
+    }
+
+    @Override
+    public Optional<Double> parse(final String value) {
+        try {
+            return Optional.of(Double.parseDouble(value));
+        } catch (final NumberFormatException e) {
+            return Optional.empty();
+        }
+    }
+
+    @Override
+    public Optional<Double> validate(final Double amount,
+                                     @Nullable final Double minAmount,
+                                     @Nullable final Double maxAmount) {
+        final boolean minBoundOk = minAmount == null || amount >= minAmount;
+        final boolean maxBoundOk = maxAmount == null || amount <= maxAmount;
+        return minBoundOk && maxBoundOk ? Optional.of(amount) : Optional.empty();
+    }
+
+    @Override
+    public Double changeAmount(final Double current,
+                               final int delta,
+                               @Nullable final Double minAmount,
+                               @Nullable final Double maxAmount) {
+        return Mth.clamp(
+            current + delta,
+            Objects.requireNonNullElse(minAmount, Double.MIN_VALUE),
+            Objects.requireNonNullElse(maxAmount, Double.MAX_VALUE)
+        );
+    }
+}
diff --git a/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/IntegerAmountOperations.java b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/IntegerAmountOperations.java
new file mode 100644
index 000000000..8630bbabb
--- /dev/null
+++ b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/IntegerAmountOperations.java
@@ -0,0 +1,49 @@
+package com.refinedmods.refinedstorage2.platform.common.screen.amount;
+
+import java.util.Objects;
+import java.util.Optional;
+import javax.annotation.Nullable;
+
+import net.minecraft.util.Mth;
+
+public class IntegerAmountOperations implements AmountOperations<Integer> {
+    public static final AmountOperations<Integer> INSTANCE = new IntegerAmountOperations();
+
+    private IntegerAmountOperations() {
+    }
+
+    @Override
+    public String format(final Integer value) {
+        return String.valueOf(value);
+    }
+
+    @Override
+    public Optional<Integer> parse(final String value) {
+        try {
+            return Optional.of(Integer.parseInt(value, 10));
+        } catch (final NumberFormatException e) {
+            return Optional.empty();
+        }
+    }
+
+    @Override
+    public Optional<Integer> validate(final Integer amount,
+                                      @Nullable final Integer minAmount,
+                                      @Nullable final Integer maxAmount) {
+        final boolean minBoundOk = minAmount == null || amount >= minAmount;
+        final boolean maxBoundOk = maxAmount == null || amount <= maxAmount;
+        return minBoundOk && maxBoundOk ? Optional.of(amount) : Optional.empty();
+    }
+
+    @Override
+    public Integer changeAmount(final Integer current,
+                                final int delta,
+                                @Nullable final Integer minAmount,
+                                @Nullable final Integer maxAmount) {
+        return Mth.clamp(
+            current + delta,
+            Objects.requireNonNullElse(minAmount, Integer.MIN_VALUE),
+            Objects.requireNonNullElse(maxAmount, Integer.MAX_VALUE)
+        );
+    }
+}
diff --git a/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/LongAmountOperations.java b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/LongAmountOperations.java
new file mode 100644
index 000000000..a933ec811
--- /dev/null
+++ b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/LongAmountOperations.java
@@ -0,0 +1,49 @@
+package com.refinedmods.refinedstorage2.platform.common.screen.amount;
+
+import java.util.Objects;
+import java.util.Optional;
+import javax.annotation.Nullable;
+
+import net.minecraft.util.Mth;
+
+public class LongAmountOperations implements AmountOperations<Long> {
+    public static final AmountOperations<Long> INSTANCE = new LongAmountOperations();
+
+    private LongAmountOperations() {
+    }
+
+    @Override
+    public String format(final Long value) {
+        return String.valueOf(value);
+    }
+
+    @Override
+    public Optional<Long> parse(final String value) {
+        try {
+            return Optional.of(Long.parseLong(value));
+        } catch (final NumberFormatException e) {
+            return Optional.empty();
+        }
+    }
+
+    @Override
+    public Optional<Long> validate(final Long amount,
+                                   @Nullable final Long minAmount,
+                                   @Nullable final Long maxAmount) {
+        final boolean minBoundOk = minAmount == null || amount >= minAmount;
+        final boolean maxBoundOk = maxAmount == null || amount <= maxAmount;
+        return minBoundOk && maxBoundOk ? Optional.of(amount) : Optional.empty();
+    }
+
+    @Override
+    public Long changeAmount(final Long current,
+                             final int delta,
+                             @Nullable final Long minAmount,
+                             @Nullable final Long maxAmount) {
+        return Mth.clamp(
+            current + delta,
+            Objects.requireNonNullElse(minAmount, Long.MIN_VALUE),
+            Objects.requireNonNullElse(maxAmount, Long.MAX_VALUE)
+        );
+    }
+}
diff --git a/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/PriorityScreen.java b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/PriorityScreen.java
index 02c02a37a..9c9f6cf6d 100644
--- a/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/PriorityScreen.java
+++ b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/PriorityScreen.java
@@ -11,7 +11,7 @@
 import static com.refinedmods.refinedstorage2.platform.common.util.IdentifierUtil.createIdentifier;
 import static com.refinedmods.refinedstorage2.platform.common.util.IdentifierUtil.createTranslation;
 
-public class PriorityScreen extends AbstractAmountScreen {
+public class PriorityScreen extends AbstractAmountScreen<AbstractAmountScreen.DefaultDummyContainerMenu, Integer> {
     private static final ResourceLocation TEXTURE = createIdentifier("textures/gui/priority.png");
     private static final MutableComponent PRIORITY_TEXT = createTranslation("gui", "priority");
 
@@ -21,10 +21,11 @@ public PriorityScreen(final ClientProperty<Integer> property,
                           final Screen parent,
                           final Inventory playerInventory) {
         super(
+            new DefaultDummyContainerMenu(),
             parent,
             playerInventory,
             PRIORITY_TEXT,
-            AmountScreenConfiguration.AmountScreenConfigurationBuilder.create()
+            AmountScreenConfiguration.AmountScreenConfigurationBuilder.<Integer>create()
                 .withInitialAmount(property.get())
                 .withIncrementsTop(1, 5, 10)
                 .withIncrementsBottom(-1, -5, -10)
@@ -33,7 +34,8 @@ public PriorityScreen(final ClientProperty<Integer> property,
                 .withMinAmount(Integer.MIN_VALUE)
                 .withMaxAmount(Integer.MAX_VALUE)
                 .withResetAmount(0)
-                .build()
+                .build(),
+            IntegerAmountOperations.INSTANCE
         );
         this.property = property;
         this.imageWidth = 164;
@@ -41,7 +43,7 @@ public PriorityScreen(final ClientProperty<Integer> property,
     }
 
     @Override
-    protected void accept(final int amount) {
+    protected void accept(final Integer amount) {
         property.setValue(amount);
     }
 
diff --git a/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/ResourceAmountScreen.java b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/ResourceAmountScreen.java
index 8b2b583a5..8b15dfb67 100644
--- a/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/ResourceAmountScreen.java
+++ b/refinedstorage2-platform-common/src/main/java/com/refinedmods/refinedstorage2/platform/common/screen/amount/ResourceAmountScreen.java
@@ -17,7 +17,7 @@
 import static com.refinedmods.refinedstorage2.platform.common.util.IdentifierUtil.createIdentifier;
 import static com.refinedmods.refinedstorage2.platform.common.util.IdentifierUtil.createTranslation;
 
-public class ResourceAmountScreen extends AbstractAmountScreen {
+public class ResourceAmountScreen extends AbstractAmountScreen<ResourceAmountScreen.DummyContainerMenu, Long> {
     private static final ResourceLocation TEXTURE = createIdentifier("textures/gui/resource_amount.png");
     private static final MutableComponent TITLE = createTranslation("gui", "amount");
 
@@ -31,32 +31,34 @@ public ResourceAmountScreen(final Screen parent,
             parent,
             playerInventory,
             TITLE,
-            AmountScreenConfiguration.AmountScreenConfigurationBuilder.create()
+            AmountScreenConfiguration.AmountScreenConfigurationBuilder.<Long>create()
                 .withInitialAmount(getInitialAmount(slot))
                 .withIncrementsTop(1, 10, 64)
                 .withIncrementsBottom(-1, -10, -64)
+                .withIncrementsBottomStartPosition(new Vector3f(7, 20 + 52, 0))
                 .withAmountFieldPosition(new Vector3f(9, 51, 0))
                 .withActionButtonsStartPosition(new Vector3f(114, 22, 0))
-                .withMinAmount(1)
+                .withMinAmount(1L)
                 .withMaxAmount(getMaxAmount(slot))
-                .withResetAmount(1)
-                .build()
+                .withResetAmount(1L)
+                .build(),
+            LongAmountOperations.INSTANCE
         );
         this.slot = slot;
         this.imageWidth = 172;
         this.imageHeight = 99;
     }
 
-    private static int getInitialAmount(final ResourceFilterSlot slot) {
-        return slot.getFilteredResource() == null ? 0 : (int) slot.getFilteredResource().getAmount();
+    private static long getInitialAmount(final ResourceFilterSlot slot) {
+        return slot.getFilteredResource() == null ? 0 : slot.getFilteredResource().getAmount();
     }
 
-    private static int getMaxAmount(final ResourceFilterSlot slot) {
-        return slot.getFilteredResource() == null ? 0 : (int) slot.getFilteredResource().getMaxAmount();
+    private static long getMaxAmount(final ResourceFilterSlot slot) {
+        return slot.getFilteredResource() == null ? 0 : slot.getFilteredResource().getMaxAmount();
     }
 
     @Override
-    protected void accept(final int amount) {
+    protected void accept(final Long amount) {
         slot.changeAmountOnClient(amount);
     }
 
@@ -78,7 +80,7 @@ protected void renderResourceFilterSlotAmount(final PoseStack poseStack,
         // should not render amount here
     }
 
-    private static class DummyContainerMenu extends AbstractContainerMenu {
+    public static class DummyContainerMenu extends AbstractContainerMenu {
         protected DummyContainerMenu(final ResourceFilterSlot slot) {
             super(null, 0);
             addSlot(slot.atPosition(89, 48));
diff --git a/refinedstorage2-platform-common/src/main/resources/assets/refinedstorage2/textures/gui/detector.png b/refinedstorage2-platform-common/src/main/resources/assets/refinedstorage2/textures/gui/detector.png
index 4c0c15500e020d80bc2bfbccfcdbccbe333be5dd..0c7e3f7fd4c627bfd980da2cf972d6e32c9c0537 100644
GIT binary patch
literal 815
zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911MRQ8&P5D>38$lZxy-8q?;Kn_c~qpu?a
z!^VE@KZ&eBzG#3?h%1o(|NsB7W5+^6L(R?2ySuxABB`6FO9LtPk|4iepeztDNO?26
z0fjgVJR*x382I*sFrx))unGeM(*#c!$B>G+x3?c=-F6UY4YXYM^<V#;T|2ib{7hVY
zZj#Tl8WFZ@8|KP{FfF?Gt@ichxcl4kr`zcY+_3ulXZrT4#{CTWvAg0<v%X{4xfkd_
zh7<QvL0)ml%NGDro5~}BB*TQ;(ku)N4R2z_K)fg4*nmnDs;7fIf~l=xKI4bi$=ho_
zY?;qg@cY7PX@(zB4X^7<ZJyUMZhF77Z$F!ZpB&=}c7I@4IegRO1{qTH`*Pm<`1>}|
zd6_%`q78rU^e<zq`L}!u|GaO`4CmO1wi{>^E+^}<oR}WbpEuJzzGjn@!vEaw7oYB)
z3$hyopqjw!`TNiPy6Y##{ABOxi-*dsXS(apUBh-_^0$nKhil#eg8>XC>^;o{Wimv3
zS6lun#n9p2ZW-O0?UO6NTsoQ6V3XM|W_RYfA578B-8wL4Lur1|Dxd{lIDjHIcdw~u
zsQg~F<-^~+XBWNIS2KRP{p{kQ_zYZHi&sOmHr!;muF7!x?>)1t%zu7P-fg{QEyE6b
w*4V1}U7J|Lzi~M{yTOps$4gJDjrq;*{At?@w|KdR%mVq=)78&qol`;+0J7RDzW@LL

literal 806
zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911MRQ8&P5D>38$lZxy-8q?;Kn_c~qpu?a
z!^VE@KZ&eBzG#3?h%1o(|NsB7W5+^6L(R?2ySuxABB`6FO9LtPk|4iepeztDNO?26
z0fjgVJR*x382Ao=Fk{xWPEG~}rgl#k$B>G+x3?O-?gWUqUhH}N<iGvRzIV?WojM=h
zd-e2<@52Mam!2neP1yDJx7qT&`*QE+6=w<DP^<mFeR@@6dBb$gyYHVe`7_M3Q($Bf
zXsDj9+Q7iXao{q$3Xm>%zllY_q2WL4hhIC{<1OwNG#p>P@4q<rhhm2BtJj|YS-_z8
z_wBXItPh@eFzhR^Lowk0&Bu@bFV{c9yo^~yh2j49K&b=Q>s@Oe)@)|v5^`W*+)*CM
z#G%l@U{mV=q#3^Jc{70YFXvw&*l<58()Pwx{`n=$ALhM#@$I%B&=nwA2ZndC(}2zc
zY6B7nuHTRUnX7)5VNZFa>Au^E;miGYG5%O0Yg#vB{amP;28KJkb-+47M8iIF%TRJO
z7(-oL&sgz&S60pb)9d`^0v&hx*@}<sx3&tyEPw?c*nJnin{9u!L^8pRiDLq9K|bS+
zdFN$=Z4b=1x2blDW<T&b*kEbDclfmrCn7ly98+VEp3RVx!?a=J(BowM{>QfZ@jorC
TAG*hZ>6F3K)z4*}Q$iB}=dCb(