diff options
author | isxander <xander@isxander.dev> | 2024-04-11 18:43:06 +0100 |
---|---|---|
committer | isxander <xander@isxander.dev> | 2024-04-11 18:43:06 +0100 |
commit | 04fe933f4c24817100f3101f088accf55a621f8a (patch) | |
tree | feff94ca3ab4484160e69a24f4ee38522381950e /src/main/java/dev/isxander/yacl3/gui/controllers/string | |
parent | 831b894fdb7fe3e173d81387c8f6a2402b8ccfa9 (diff) | |
download | YetAnotherConfigLib-04fe933f4c24817100f3101f088accf55a621f8a.tar.gz YetAnotherConfigLib-04fe933f4c24817100f3101f088accf55a621f8a.tar.bz2 YetAnotherConfigLib-04fe933f4c24817100f3101f088accf55a621f8a.zip |
Extremely fragile and broken multiversion build with stonecutter
Diffstat (limited to 'src/main/java/dev/isxander/yacl3/gui/controllers/string')
9 files changed, 1081 insertions, 0 deletions
diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/string/IStringController.java b/src/main/java/dev/isxander/yacl3/gui/controllers/string/IStringController.java new file mode 100644 index 0000000..14d10dd --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/controllers/string/IStringController.java @@ -0,0 +1,44 @@ +package dev.isxander.yacl3.gui.controllers.string; + +import dev.isxander.yacl3.api.Controller; +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.utils.Dimension; +import dev.isxander.yacl3.gui.AbstractWidget; +import dev.isxander.yacl3.gui.YACLScreen; +import net.minecraft.network.chat.Component; + +/** + * A controller that can be any type but can input and output a string. + */ +public interface IStringController<T> extends Controller<T> { + /** + * Gets the option's pending value as a string. + * + * @see Option#pendingValue() + */ + String getString(); + + /** + * Sets the option's pending value from a string. + * + * @see Option#requestSet(Object) + */ + void setFromString(String value); + + /** + * {@inheritDoc} + */ + @Override + default Component formatValue() { + return Component.literal(getString()); + } + + default boolean isInputValid(String input) { + return true; + } + + @Override + default AbstractWidget provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) { + return new StringControllerElement(this, screen, widgetDimension, true); + } +} diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/string/StringController.java b/src/main/java/dev/isxander/yacl3/gui/controllers/string/StringController.java new file mode 100644 index 0000000..4bafc0f --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/controllers/string/StringController.java @@ -0,0 +1,37 @@ +package dev.isxander.yacl3.gui.controllers.string; + +import dev.isxander.yacl3.api.Option; + +/** + * A custom text field implementation for strings. + */ +public class StringController implements IStringController<String> { + private final Option<String> option; + + /** + * Constructs a string controller + * + * @param option bound option + */ + public StringController(Option<String> option) { + this.option = option; + } + + /** + * {@inheritDoc} + */ + @Override + public Option<String> option() { + return option; + } + + @Override + public String getString() { + return option().pendingValue(); + } + + @Override + public void setFromString(String value) { + option().requestSet(value); + } +} diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/string/StringControllerElement.java b/src/main/java/dev/isxander/yacl3/gui/controllers/string/StringControllerElement.java new file mode 100644 index 0000000..689d8e2 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/controllers/string/StringControllerElement.java @@ -0,0 +1,466 @@ +package dev.isxander.yacl3.gui.controllers.string; + +import com.mojang.blaze3d.platform.InputConstants; +import dev.isxander.yacl3.api.utils.Dimension; +import dev.isxander.yacl3.gui.YACLScreen; +import dev.isxander.yacl3.gui.controllers.ControllerWidget; +import dev.isxander.yacl3.gui.utils.GuiUtils; +import dev.isxander.yacl3.gui.utils.UndoRedoHelper; +import net.minecraft.ChatFormatting; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; + +import java.util.function.Consumer; + +public class StringControllerElement extends ControllerWidget<IStringController<?>> { + protected final boolean instantApply; + + protected String inputField; + protected Dimension<Integer> inputFieldBounds; + protected boolean inputFieldFocused; + + protected int caretPos; + protected int previousCaretPos; + protected int selectionLength; + protected int renderOffset; + + protected UndoRedoHelper undoRedoHelper; + + protected float ticks; + protected float caretTicks; + + private final Component emptyText; + + public StringControllerElement(IStringController<?> control, YACLScreen screen, Dimension<Integer> dim, boolean instantApply) { + super(control, screen, dim); + this.instantApply = instantApply; + inputField = control.getString(); + inputFieldFocused = false; + selectionLength = 0; + emptyText = Component.literal("Click to type...").withStyle(ChatFormatting.GRAY); + control.option().addListener((opt, val) -> { + inputField = control.getString(); + }); + setDimension(dim); + } + + @Override + protected void drawHoveredControl(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + + } + + @Override + protected void drawValueText(GuiGraphics graphics, int mouseX, int mouseY, float delta) { + Component valueText = getValueText(); + if (!isHovered()) valueText = Component.literal(GuiUtils.shortenString(valueText.getString(), textRenderer, getMaxUnwrapLength(), "...")).setStyle(valueText.getStyle()); + + int textX = getDimension().xLimit() - textRenderer.width(valueText) + renderOffset - getXPadding(); + graphics.enableScissor(inputFieldBounds.x(), inputFieldBounds.y() - 2, inputFieldBounds.xLimit() + 1, inputFieldBounds.yLimit() + 4); + graphics.drawString(textRenderer, valueText, textX, getTextY(), getValueColor(), true); + + if (isHovered()) { + ticks += delta; + + String text = getValueText().getString(); + + graphics.fill(inputFieldBounds.x(), inputFieldBounds.yLimit(), inputFieldBounds.xLimit(), inputFieldBounds.yLimit() + 1, -1); + graphics.fill(inputFieldBounds.x() + 1, inputFieldBounds.yLimit() + 1, inputFieldBounds.xLimit() + 1, inputFieldBounds.yLimit() + 2, 0xFF404040); + + if (inputFieldFocused || focused) { + if (caretPos > text.length()) + caretPos = text.length(); + + int caretX = textX + textRenderer.width(text.substring(0, caretPos)); + if (text.isEmpty()) + caretX = inputFieldBounds.x() + inputFieldBounds.width() / 2; + + if (selectionLength != 0) { + int selectionX = textX + textRenderer.width(text.substring(0, caretPos + selectionLength)); + graphics.fill(caretX, inputFieldBounds.y() - 2, selectionX, inputFieldBounds.yLimit() - 1, 0x803030FF); + } + + if(caretPos != previousCaretPos) { + previousCaretPos = caretPos; + caretTicks = 0; + } + + if ((caretTicks += delta) % 20 <= 10) + graphics.fill(caretX, inputFieldBounds.y() - 2, caretX + 1, inputFieldBounds.yLimit() - 1, -1); + } + } + graphics.disableScissor(); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (isAvailable() && getDimension().isPointInside((int) mouseX, (int) mouseY)) { + inputFieldFocused = true; + + if (!inputFieldBounds.isPointInside((int) mouseX, (int) mouseY)) { + caretPos = getDefaultCaretPos(); + } else { + // gets the appropriate caret position for where you click + int textX = (int) mouseX - (inputFieldBounds.xLimit() - textRenderer.width(getValueText())); + int pos = -1; + int currentWidth = 0; + for (char ch : inputField.toCharArray()) { + pos++; + int charLength = textRenderer.width(String.valueOf(ch)); + if (currentWidth + charLength / 2 > textX) { // if more than halfway past the characters select in front of that char + caretPos = pos; + break; + } else if (pos == inputField.length() - 1) { + // if we have reached the end and no matches, it must be the second half of the char so the last position + caretPos = pos + 1; + } + currentWidth += charLength; + } + + selectionLength = 0; + } +// if (undoRedoHelper == null) { +// undoRedoHelper = new UndoRedoHelper(inputField, caretPos, selectionLength); +// } + + return true; + } else { + unfocus(); + } + + return false; + } + + protected int getDefaultCaretPos() { + return inputField.length(); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!inputFieldFocused) + return false; + + switch (keyCode) { + case InputConstants.KEY_ESCAPE, InputConstants.KEY_RETURN -> { + unfocus(); + return true; + } + case InputConstants.KEY_LEFT -> { + if (Screen.hasShiftDown()) { + if (Screen.hasControlDown()) { + int spaceChar = findSpaceIndex(true); + selectionLength += caretPos - spaceChar; + caretPos = spaceChar; + } else if (caretPos > 0) { + caretPos--; + selectionLength += 1; + } + checkRenderOffset(); + } else { + if (caretPos > 0) { + if (Screen.hasControlDown()) { + caretPos = findSpaceIndex(true); + } else { + if (selectionLength != 0) { + caretPos += Math.min(selectionLength, 0); + } else caretPos--; + } + } + checkRenderOffset(); + selectionLength = 0; + } + + return true; + } + case InputConstants.KEY_RIGHT -> { + if (Screen.hasShiftDown()) { + if (Screen.hasControlDown()) { + int spaceChar = findSpaceIndex(false); + selectionLength -= spaceChar - caretPos; + caretPos = spaceChar; + } else if (caretPos < inputField.length()) { + caretPos++; + selectionLength -= 1; + } + checkRenderOffset(); + } else { + if (caretPos < inputField.length()) { + if (Screen.hasControlDown()) { + caretPos = findSpaceIndex(false); + } else { + if (selectionLength != 0) { + caretPos += Math.max(selectionLength, 0); + } else caretPos++; + } + checkRenderOffset(); + } + selectionLength = 0; + } + + return true; + } + case InputConstants.KEY_BACKSPACE -> { + doBackspace(); + return true; + } + case InputConstants.KEY_DELETE -> { + doDelete(); + return true; + } + case InputConstants.KEY_END -> { + if (Screen.hasShiftDown()) { + selectionLength -= inputField.length() - caretPos; + } else selectionLength = 0; + caretPos = inputField.length(); + checkRenderOffset(); + return true; + } + case InputConstants.KEY_HOME -> { + if (Screen.hasShiftDown()) { + selectionLength += caretPos; + caretPos = 0; + } else { + caretPos = 0; + selectionLength = 0; + } + checkRenderOffset(); + return true; + } +// case InputConstants.KEY_Z -> { +// if (Screen.hasControlDown()) { +// UndoRedoHelper.FieldState updated = Screen.hasShiftDown() ? undoRedoHelper.redo() : undoRedoHelper.undo(); +// if (updated != null) { +// System.out.println("Updated: " + updated); +// if (modifyInput(builder -> builder.replace(0, inputField.length(), updated.text()))) { +// caretPos = updated.cursorPos(); +// selectionLength = updated.selectionLength(); +// checkRenderOffset(); +// } +// } +// return true; +// } +// } + } + + if (Screen.isPaste(keyCode)) { + return doPaste(); + } else if (Screen.isCopy(keyCode)) { + return doCopy(); + } else if (Screen.isCut(keyCode)) { + return doCut(); + } else if (Screen.isSelectAll(keyCode)) { + return doSelectAll(); + } + + return false; + } + + protected boolean doPaste() { + this.write(client.keyboardHandler.getClipboard()); + updateUndoHistory(); + return true; + } + + protected boolean doCopy() { + if (selectionLength != 0) { + client.keyboardHandler.setClipboard(getSelection()); + return true; + } + return false; + } + + protected boolean doCut() { + if (selectionLength != 0) { + client.keyboardHandler.setClipboard(getSelection()); + this.write(""); + updateUndoHistory(); + return true; + } + return false; + } + + protected boolean doSelectAll() { + caretPos = inputField.length(); + checkRenderOffset(); + selectionLength = -caretPos; + return true; + } + + protected void checkRenderOffset() { + if (textRenderer.width(inputField) < getUnshiftedLength()) { + renderOffset = 0; + return; + } + + int textX = getDimension().xLimit() - textRenderer.width(inputField) - getXPadding(); + int caretX = textX + textRenderer.width(inputField.substring(0, caretPos)); + + int minX = getDimension().xLimit() - getXPadding() - getUnshiftedLength(); + int maxX = minX + getUnshiftedLength(); + + if (caretX + renderOffset < minX) { + renderOffset = minX - caretX; + } else if (caretX + renderOffset > maxX) { + renderOffset = maxX - caretX; + } + } + + @Override + public boolean charTyped(char chr, int modifiers) { + if (!inputFieldFocused) + return false; + + if (!Screen.hasControlDown()) { + write(Character.toString(chr)); + updateUndoHistory(); + return true; + } + + return false; + } + + protected void doBackspace() { + if (selectionLength != 0) { + write(""); + } else if (caretPos > 0) { + if (modifyInput(builder -> builder.deleteCharAt(caretPos - 1))) { + caretPos--; + checkRenderOffset(); + } + } + updateUndoHistory(); + } + + protected void doDelete() { + if (selectionLength != 0) { + write(""); + } else if (caretPos < inputField.length()) { + modifyInput(builder -> builder.deleteCharAt(caretPos)); + } + updateUndoHistory(); + } + + public void write(String string) { + if (selectionLength == 0) { + if (modifyInput(builder -> builder.insert(caretPos, string))) { + caretPos += string.length(); + checkRenderOffset(); + } + } else { + int start = getSelectionStart(); + int end = getSelectionEnd(); + + if (modifyInput(builder -> builder.replace(start, end, string))) { + caretPos = start + string.length(); + selectionLength = 0; + checkRenderOffset(); + } + } + } + + public boolean modifyInput(Consumer<StringBuilder> consumer) { + StringBuilder temp = new StringBuilder(inputField); + consumer.accept(temp); + if (!control.isInputValid(temp.toString())) + return false; + inputField = temp.toString(); + if (instantApply) + updateControl(); + return true; + } + + protected void updateUndoHistory() { +// undoRedoHelper.save(inputField, caretPos, selectionLength); + } + + public int getUnshiftedLength() { + if (optionNameString.isEmpty()) + return getDimension().width() - getXPadding() * 2; + return getDimension().width() / 8 * 5; + } + + public int getMaxUnwrapLength() { + if (optionNameString.isEmpty()) + return getDimension().width() - getXPadding() * 2; + return getDimension().width() / 2; + } + + public int getSelectionStart() { + return Math.min(caretPos, caretPos + selectionLength); + } + + public int getSelectionEnd() { + return Math.max(caretPos, caretPos + selectionLength); + } + + protected String getSelection() { + return inputField.substring(getSelectionStart(), getSelectionEnd()); + } + + protected int findSpaceIndex(boolean reverse) { + int i; + int fromIndex = caretPos; + if (reverse) { + if (caretPos > 0) + fromIndex -= 2; + i = this.inputField.lastIndexOf(" ", fromIndex) + 1; + } else { + if (caretPos < inputField.length()) + fromIndex += 1; + i = this.inputField.indexOf(" ", fromIndex) + 1; + + if (i == 0) i = inputField.length(); + } + + return i; + } + + @Override + public void setFocused(boolean focused) { + super.setFocused(focused); + inputFieldFocused = focused; + } + + @Override + public void unfocus() { + super.unfocus(); + inputFieldFocused = false; + renderOffset = 0; + if (!instantApply) updateControl(); + } + + @Override + public void setDimension(Dimension<Integer> dim) { + super.setDimension(dim); + + int width = Math.max(6, Math.min(textRenderer.width(getValueText()), getUnshiftedLength())); + inputFieldBounds = Dimension.ofInt(dim.xLimit() - getXPadding() - width, dim.centerY() - textRenderer.lineHeight / 2, width, textRenderer.lineHeight); + } + + @Override + public boolean isHovered() { + return super.isHovered() || inputFieldFocused; + } + + protected void updateControl() { + control.setFromString(inputField); + } + + @Override + protected int getUnhoveredControlWidth() { + return !isHovered() ? Math.min(getHoveredControlWidth(), getMaxUnwrapLength()) : getHoveredControlWidth(); + } + + @Override + protected int getHoveredControlWidth() { + return Math.min(textRenderer.width(getValueText()), getUnshiftedLength()); + } + + @Override + protected Component getValueText() { + if (!inputFieldFocused && inputField.isEmpty()) + return emptyText; + + return instantApply || !inputFieldFocused ? control.formatValue() : Component.literal(inputField); + } +} diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/string/number/DoubleFieldController.java b/src/main/java/dev/isxander/yacl3/gui/controllers/string/number/DoubleFieldController.java new file mode 100644 index 0000000..1fe3e41 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/controllers/string/number/DoubleFieldController.java @@ -0,0 +1,111 @@ +package dev.isxander.yacl3.gui.controllers.string.number; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ValueFormatter; +import dev.isxander.yacl3.gui.controllers.slider.DoubleSliderController; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.Function; + +/** + * {@inheritDoc} + */ +public class DoubleFieldController extends NumberFieldController<Double> { + private final double min, max; + + /** + * Constructs a double field controller + * + * @param option option to bind controller to + * @param min minimum allowed value (clamped on apply) + * @param max maximum allowed value (clamped on apply) + * @param formatter display text, not used whilst editing + */ + public DoubleFieldController(Option<Double> option, double min, double max, Function<Double, Component> formatter) { + super(option, formatter); + this.min = min; + this.max = max; + } + + /** + * Constructs a double field controller. + * Uses {@link DoubleSliderController#DEFAULT_FORMATTER} as display text, + * not used whilst editing. + * + * @param option option to bind controller to + * @param min minimum allowed value (clamped on apply) + * @param max maximum allowed value (clamped on apply) + */ + public DoubleFieldController(Option<Double> option, double min, double max) { + this(option, min, max, DoubleSliderController.DEFAULT_FORMATTER); + } + + /** + * Constructs a double field controller. + * Does not have a minimum or a maximum range. + * + * @param option option to bind controller to + * @param formatter display text, not used whilst editing + */ + public DoubleFieldController(Option<Double> option, Function<Double, Component> formatter) { + this(option, -Double.MAX_VALUE, Double.MAX_VALUE, formatter); + } + + /** + * Constructs a double field controller. + * Uses {@link DoubleSliderController#DEFAULT_FORMATTER} as display text, + * not used whilst editing. + * Does not have a minimum or a maximum range. + * + * @param option option to bind controller to + */ + public DoubleFieldController(Option<Double> option) { + this(option, -Double.MAX_VALUE, Double.MAX_VALUE, DoubleSliderController.DEFAULT_FORMATTER); + } + + @ApiStatus.Internal + public static DoubleFieldController createInternal(Option<Double> option, double min, double max, ValueFormatter<Double> formatter) { + return new DoubleFieldController(option, min, max, formatter::format); + } + + /** + * {@inheritDoc} + */ + @Override + public double min() { + return this.min; + } + + /** + * {@inheritDoc} + */ + @Override + public double max() { + return this.max; + } + + /** + * {@inheritDoc} + */ + @Override + public String getString() { + return NUMBER_FORMAT.format(option().pendingValue()); + } + + /** + * {@inheritDoc} + */ + @Override + public void setPendingValue(double value) { + option().requestSet(value); + } + + /** + * {@inheritDoc} + */ + @Override + public double pendingValue() { + return option().pendingValue(); + } +} diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/string/number/FloatFieldController.java b/src/main/java/dev/isxander/yacl3/gui/controllers/string/number/FloatFieldController.java new file mode 100644 index 0000000..8c81b49 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/controllers/string/number/FloatFieldController.java @@ -0,0 +1,111 @@ +package dev.isxander.yacl3.gui.controllers.string.number; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ValueFormatter; +import dev.isxander.yacl3.gui.controllers.slider.FloatSliderController; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.Function; + +/** + * {@inheritDoc} + */ +public class FloatFieldController extends NumberFieldController<Float> { + private final float min, max; + + /** + * Constructs a float field controller + * + * @param option option to bind controller to + * @param min minimum allowed value (clamped on apply) + * @param max maximum allowed value (clamped on apply) + * @param formatter display text, not used whilst editing + */ + public FloatFieldController(Option<Float> option, float min, float max, Function<Float, Component> formatter) { + super(option, formatter); + this.min = min; + this.max = max; + } + + /** + * Constructs a float field controller. + * Uses {@link FloatSliderController#DEFAULT_FORMATTER} as display text, + * not used whilst editing. + * + * @param option option to bind controller to + * @param min minimum allowed value (clamped on apply) + * @param max maximum allowed value (clamped on apply) + */ + public FloatFieldController(Option<Float> option, float min, float max) { + this(option, min, max, FloatSliderController.DEFAULT_FORMATTER); + } + + /** + * Constructs a float field controller. + * Does not have a minimum or a maximum range. + * + * @param option option to bind controller to + * @param formatter display text, not used whilst editing + */ + public FloatFieldController(Option<Float> option, Function<Float, Component> formatter) { + this(option, -Float.MAX_VALUE, Float.MAX_VALUE, formatter); + } + + /** + * Constructs a float field controller. + * Uses {@link FloatSliderController#DEFAULT_FORMATTER} as display text, + * not used whilst editing. + * Does not have a minimum or a maximum range. + * + * @param option option to bind controller to + */ + public FloatFieldController(Option<Float> option) { + this(option, -Float.MAX_VALUE, Float.MAX_VALUE, FloatSliderController.DEFAULT_FORMATTER); + } + + @ApiStatus.Internal + public static FloatFieldController createInternal(Option<Float> option, float min, float max, ValueFormatter<Float> formatter) { + return new FloatFieldController(option, min, max, formatter::format); + } + + /** + * {@inheritDoc} + */ + @Override + public double min() { + return this.min; + } + + /** + * {@inheritDoc} + */ + @Override + public double max() { + return this.max; + } + + /** + * {@inheritDoc} + */ + @Override + public String getString() { + return NUMBER_FORMAT.format(option().pendingValue()); + } + + /** + * {@inheritDoc} + */ + @Override + public void setPendingValue(double value) { + option().requestSet((float) value); + } + + /** + * {@inheritDoc} + */ + @Override + public double pendingValue() { + return option().pendingValue(); + } +} diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/string/number/IntegerFieldController.java b/src/main/java/dev/isxander/yacl3/gui/controllers/string/number/IntegerFieldController.java new file mode 100644 index 0000000..6286978 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/controllers/string/number/IntegerFieldController.java @@ -0,0 +1,111 @@ +package dev.isxander.yacl3.gui.controllers.string.number; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ValueFormatter; +import dev.isxander.yacl3.gui.controllers.slider.IntegerSliderController; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.Function; + +/** + * {@inheritDoc} + */ +public class IntegerFieldController extends NumberFieldController<Integer> { + private final int min, max; + + /** + * Constructs a integer field controller + * + * @param option option to bind controller to + * @param min minimum allowed value (clamped on apply) + * @param max maximum allowed value (clamped on apply) + * @param formatter display text, not used whilst editing + */ + public IntegerFieldController(Option<Integer> option, int min, int max, Function<Integer, Component> formatter) { + super(option, formatter); + this.min = min; + this.max = max; + } + + /** + * Constructs a integer field controller. + * Uses {@link IntegerSliderController#DEFAULT_FORMATTER} as display text, + * not used whilst editing. + * + * @param option option to bind controller to + * @param min minimum allowed value (clamped on apply) + * @param max maximum allowed value (clamped on apply) + */ + public IntegerFieldController(Option<Integer> option, int min, int max) { + this(option, min, max, IntegerSliderController.DEFAULT_FORMATTER); + } + + /** + * Constructs a integer field controller. + * Does not have a minimum or a maximum range. + * + * @param option option to bind controller to + * @param formatter display text, not used whilst editing + */ + public IntegerFieldController(Option<Integer> option, Function<Integer, Component> formatter) { + this(option, -Integer.MAX_VALUE, Integer.MAX_VALUE, formatter); + } + + /** + * Constructs a integer field controller. + * Uses {@link IntegerSliderController#DEFAULT_FORMATTER} as display text, + * not used whilst editing. + * Does not have a minimum or a maximum range. + * + * @param option option to bind controller to + */ + public IntegerFieldController(Option<Integer> option) { + this(option, -Integer.MAX_VALUE, Integer.MAX_VALUE, IntegerSliderController.DEFAULT_FORMATTER); + } + + @ApiStatus.Internal + public static IntegerFieldController createInternal(Option<Integer> option, int min, int max, ValueFormatter<Integer> formatter) { + return new IntegerFieldController(option, min, max, formatter::format); + } + + /** + * {@inheritDoc} + */ + @Override + public double min() { + return this.min; + } + + /** + * {@inheritDoc} + */ + @Override + public double max() { + return this.max; + } + + /** + * {@inheritDoc} + */ + @Override + public String getString() { + return NUMBER_FORMAT.format(option().pendingValue()); + } + + /** + * {@inheritDoc} + */ + @Override + public void setPendingValue(double value) { + option().requestSet((int) value); + } + + /** + * {@inheritDoc} + */ + @Override + public double pendingValue() { + return option().pendingValue(); + } +} diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/string/number/LongFieldController.java b/src/main/java/dev/isxander/yacl3/gui/controllers/string/number/LongFieldController.java new file mode 100644 index 0000000..906a2b5 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/controllers/string/number/LongFieldController.java @@ -0,0 +1,111 @@ +package dev.isxander.yacl3.gui.controllers.string.number; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ValueFormatter; +import dev.isxander.yacl3.gui.controllers.slider.LongSliderController; +import net.minecraft.network.chat.Component; +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.Function; + +/** + * {@inheritDoc} + */ +public class LongFieldController extends NumberFieldController<Long> { + private final long min, max; + + /** + * Constructs a long field controller + * + * @param option option to bind controller to + * @param min minimum allowed value (clamped on apply) + * @param max maximum allowed value (clamped on apply) + * @param formatter display text, not used whilst editing + */ + public LongFieldController(Option<Long> option, long min, long max, Function<Long, Component> formatter) { + super(option, formatter); + this.min = min; + this.max = max; + } + + /** + * Constructs a long field controller. + * Uses {@link LongSliderController#DEFAULT_FORMATTER} as display text, + * not used whilst editing. + * + * @param option option to bind controller to + * @param min minimum allowed value (clamped on apply) + * @param max maximum allowed value (clamped on apply) + */ + public LongFieldController(Option<Long> option, long min, long max) { + this(option, min, max, LongSliderController.DEFAULT_FORMATTER); + } + + /** + * Constructs a long field controller. + * Does not have a minimum or a maximum range. + * + * @param option option to bind controller to + * @param formatter display text, not used whilst editing + */ + public LongFieldController(Option<Long> option, Function<Long, Component> formatter) { + this(option, -Long.MAX_VALUE, Long.MAX_VALUE, formatter); + } + + /** + * Constructs a long field controller. + * Uses {@link LongSliderController#DEFAULT_FORMATTER} as display text, + * not used whilst editing. + * Does not have a minimum or a maximum range. + * + * @param option option to bind controller to + */ + public LongFieldController(Option<Long> option) { + this(option, -Long.MAX_VALUE, Long.MAX_VALUE, LongSliderController.DEFAULT_FORMATTER); + } + + @ApiStatus.Internal + public static LongFieldController createInternal(Option<Long> option, long min, long max, ValueFormatter<Long> formatter) { + return new LongFieldController(option, min, max, formatter::format); + } + + /** + * {@inheritDoc} + */ + @Override + public double min() { + return this.min; + } + + /** + * {@inheritDoc} + */ + @Override + public double max() { + return this.max; + } + + /** + * {@inheritDoc} + */ + @Override + public String getString() { + return NUMBER_FORMAT.format(option().pendingValue()); + } + + /** + * {@inheritDoc} + */ + @Override + public void setPendingValue(double value) { + option().requestSet((long) value); + } + + /** + * {@inheritDoc} + */ + @Override + public double pendingValue() { + return option().pendingValue(); + } +} diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/string/number/NumberFieldController.java b/src/main/java/dev/isxander/yacl3/gui/controllers/string/number/NumberFieldController.java new file mode 100644 index 0000000..3c06876 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/controllers/string/number/NumberFieldController.java @@ -0,0 +1,80 @@ +package dev.isxander.yacl3.gui.controllers.string.number; + +import dev.isxander.yacl3.api.Option; +import dev.isxander.yacl3.api.controller.ValueFormatter; +import dev.isxander.yacl3.api.utils.Dimension; +import dev.isxander.yacl3.gui.AbstractWidget; +import dev.isxander.yacl3.gui.YACLScreen; +import dev.isxander.yacl3.gui.controllers.slider.ISliderController; +import dev.isxander.yacl3.gui.controllers.string.IStringController; +import dev.isxander.yacl3.gui.controllers.string.StringControllerElement; +import dev.isxander.yacl3.impl.utils.YACLConstants; +import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; + +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.ParsePosition; +import java.util.function.Function; + +/** + * Controller that allows you to enter in numbers using a text field. + * + * @param <T> number type + */ +public abstract class NumberFieldController<T extends Number> implements ISliderController<T>, IStringController<T> { + + protected static final NumberFormat NUMBER_FORMAT = NumberFormat.getInstance(); + private static final DecimalFormatSymbols DECIMAL_FORMAT_SYMBOLS = DecimalFormatSymbols.getInstance(); + + private final Option<T> option; + private final ValueFormatter<T> displayFormatter; + + public NumberFieldController(Option<T> option, Function<T, Component> displayFormatter) { + this.option = option; + this.displayFormatter = displayFormatter::apply; + } + + @Override + public Option<T> option() { + return this.option; + } + + @Override + public void setFromString(String value) { + try { + setPendingValue(Mth.clamp(NUMBER_FORMAT.parse(value).doubleValue(), min(), max())); + } catch (ParseException ignore) { + YACLConstants.LOGGER.warn("Failed to parse number: {}", value); + } + } + + @Override + public double pendingValue() { + return option().pendingValue().doubleValue(); + } + + @Override + public boolean isInputValid(String input) { + input = input.replace(DECIMAL_FORMAT_SYMBOLS.getGroupingSeparator() + "", ""); + ParsePosition parsePosition = new ParsePosition(0); + NUMBER_FORMAT.parse(input, parsePosition); + return parsePosition.getIndex() == input.length(); + } + + @Override + public Component formatValue() { + return displayFormatter.format(option().pendingValue()); + } + + @Override + public AbstractWidget provideWidget(YACLScreen screen, Dimension<Integer> widgetDimension) { + return new StringControllerElement(this, screen, widgetDimension, false); + } + + @Override + public double interval() { + return -1; + } +} diff --git a/src/main/java/dev/isxander/yacl3/gui/controllers/string/number/package-info.java b/src/main/java/dev/isxander/yacl3/gui/controllers/string/number/package-info.java new file mode 100644 index 0000000..4d8bbc2 --- /dev/null +++ b/src/main/java/dev/isxander/yacl3/gui/controllers/string/number/package-info.java @@ -0,0 +1,10 @@ +/** + * This package contains implementations of input fields for different number types + * <ul> + * <li>For doubles: {@link dev.isxander.yacl3.gui.controllers.string.number.DoubleFieldController}</li> + * <li>For floats: {@link dev.isxander.yacl3.gui.controllers.string.number.FloatFieldController}</li> + * <li>For integers: {@link dev.isxander.yacl3.gui.controllers.string.number.IntegerFieldController}</li> + * <li>For longs: {@link dev.isxander.yacl3.gui.controllers.string.number.LongFieldController}</li> + * </ul> + */ +package dev.isxander.yacl3.gui.controllers.string.number; |