From f2b24066288fae2095ca92c166486d74b3b16ff1 Mon Sep 17 00:00:00 2001 From: viciscat <51047087+viciscat@users.noreply.github.com> Date: Sat, 26 Jul 2025 23:31:43 +0200 Subject: Rename item GUI (#1490) * i need to change branch * done implementing this stuff * i am hilarious * better selecting and move text around * Update ARGBTextInput.java * fix NPE * small fix --- .../skyblock/item/custom/CustomItemNames.java | 45 +- .../item/custom/screen/name/ColorPopup.java | 133 ++++++ .../custom/screen/name/CustomizeNameScreen.java | 464 +++++++++++++++++++++ .../custom/screen/name/visitor/BaseVisitor.java | 24 ++ .../name/visitor/GetClickedPositionVisitor.java | 57 +++ .../screen/name/visitor/GetRenderWidthVisitor.java | 58 +++ .../screen/name/visitor/GetStyleVisitor.java | 46 ++ .../screen/name/visitor/InsertTextVisitor.java | 60 +++ .../screen/name/visitor/SetStyleVisitor.java | 57 +++ .../skyblocker/utils/render/gui/ARGBTextInput.java | 34 +- .../utils/render/gui/ColorPickerWidget.java | 43 +- 11 files changed, 995 insertions(+), 26 deletions(-) create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/ColorPopup.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/CustomizeNameScreen.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/BaseVisitor.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/GetClickedPositionVisitor.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/GetRenderWidthVisitor.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/GetStyleVisitor.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/InsertTextVisitor.java create mode 100644 src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/visitor/SetStyleVisitor.java (limited to 'src/main/java') diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomItemNames.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomItemNames.java index 2cb7ca91..d1a6d4d5 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomItemNames.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomItemNames.java @@ -5,15 +5,18 @@ import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.StringArgumentType; import de.hysky.skyblocker.annotations.Init; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.item.custom.screen.name.CustomizeNameScreen; import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.scheduler.Scheduler; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; import net.minecraft.command.CommandRegistryAccess; import net.minecraft.command.argument.TextArgumentType; +import net.minecraft.item.ItemStack; import net.minecraft.text.MutableText; import net.minecraft.text.Style; import net.minecraft.text.Text; @@ -28,7 +31,7 @@ public class CustomItemNames { dispatcher.register(ClientCommandManager.literal("skyblocker") .then(ClientCommandManager.literal("custom") .then(ClientCommandManager.literal("renameItem") - .executes(context -> renameItem(context.getSource(), null)) + .executes(context -> openScreen(context.getSource())) .then(ClientCommandManager.argument("textComponent", TextArgumentType.text(registryAccess)) .executes(context -> renameItem(context.getSource(), context.getArgument("textComponent", Text.class)))) // greedy string will only consume the arg if the text component parsing fails. @@ -36,24 +39,32 @@ public class CustomItemNames { .executes(context -> renameItem(context.getSource(), Text.of(context.getArgument("basicText", String.class)))))))); } + private static int openScreen(FabricClientCommandSource source) { + if (!Utils.isOnSkyblock()) { + source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.notOnSkyblock"))); + return 0; + } + ItemStack handStack = source.getPlayer().getMainHandStack(); + if (handStack.isEmpty()) { + source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.noItem"))); + return 0; + } + if (ItemUtils.getItemUuid(handStack).isEmpty()) { + source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.noItemUuid"))); + return 0; + } + Scheduler.queueOpenScreen(new CustomizeNameScreen(handStack)); + return Command.SINGLE_SUCCESS; + } + @SuppressWarnings("SameReturnValue") private static int renameItem(FabricClientCommandSource source, Text text) { if (Utils.isOnSkyblock()) { String itemUuid = ItemUtils.getItemUuid(source.getPlayer().getMainHandStack()); if (!itemUuid.isEmpty()) { - Object2ObjectOpenHashMap customItemNames = SkyblockerConfigManager.get().general.customItemNames; - - if (text == null) { - if (customItemNames.containsKey(itemUuid)) { - //Remove custom item name when the text argument isn't passed - customItemNames.remove(itemUuid); - SkyblockerConfigManager.save(); - source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.removed"))); - } else { - source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.neverHad"))); - } - } else { + SkyblockerConfigManager.update(config -> { + Object2ObjectOpenHashMap customItemNames = config.general.customItemNames; //If the text is provided then set the item's custom name to it //Set italic to false if it hasn't been changed (or was already false) @@ -61,14 +72,14 @@ public class CustomItemNames { ((MutableText) text).setStyle(currentStyle.withItalic(currentStyle.isItalic())); customItemNames.put(itemUuid, text); - SkyblockerConfigManager.save(); - source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.added"))); - } + }); + source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.added"))); + } else { source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.noItemUuid"))); } } else { - source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.unableToSetName"))); + source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.customItemNames.notOnSkyblock"))); } return Command.SINGLE_SUCCESS; diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/ColorPopup.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/ColorPopup.java new file mode 100644 index 00000000..9c8ffa69 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/ColorPopup.java @@ -0,0 +1,133 @@ +package de.hysky.skyblocker.skyblock.item.custom.screen.name; + + +import de.hysky.skyblocker.utils.render.gui.ARGBTextInput; +import de.hysky.skyblocker.utils.render.gui.AbstractPopupScreen; +import de.hysky.skyblocker.utils.render.gui.ColorPickerWidget; +import it.unimi.dsi.fastutil.ints.IntIntMutablePair; +import it.unimi.dsi.fastutil.ints.IntIntPair; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.*; +import net.minecraft.text.Text; + +import java.util.function.IntConsumer; + +public class ColorPopup extends AbstractPopupScreen { + + private final GridWidget layout = new GridWidget(); + + private final boolean gradient; + private final GradientConsumer gradientConsumer; + private final IntIntPair currentColor = new IntIntMutablePair(-1, -1); + + private ColorPopup(Screen backgroundScreen, GradientConsumer gradientConsumer, boolean gradient) { + super(Text.literal("Color Popup"), backgroundScreen); + this.gradientConsumer = gradientConsumer; + this.gradient = gradient; + layout.getMainPositioner().alignHorizontalCenter(); + } + + private ColorPopup(Screen backgroundScreen, IntConsumer consumer) { + this(backgroundScreen, ((start, end) -> consumer.accept(start)), false); + } + + public static ColorPopup create(Screen backgroundScreen, IntConsumer colorConsumer) { + return new ColorPopup(backgroundScreen, colorConsumer); + } + + public static ColorPopup createGradient(Screen backgroundScreen, GradientConsumer gradientConsumer) { + return new ColorPopup(backgroundScreen, gradientConsumer, true); + } + + @Override + protected void init() { + GridWidget.Adder adder = layout.createAdder(2); + addDrawableChild(adder.add(new TextWidget(Text.translatable("skyblocker.customItemNames.screen.customColorTitle"), textRenderer), 2)); + if (gradient) { + createLayoutGradient(adder); + } else { + createLayout(adder); + } + adder.add(EmptyWidget.ofHeight(15), 2); + addDrawableChild(adder.add(ButtonWidget.builder(Text.translatable("gui.cancel"), b -> close()).build(), Positioner.create().alignRight().marginRight(2))); + addDrawableChild(adder.add(ButtonWidget.builder(Text.translatable("gui.done"), b -> { + gradientConsumer.accept(currentColor.firstInt(), currentColor.secondInt()); + close(); + }).build(), Positioner.create().alignLeft().marginLeft(2))); + super.init(); + } + + @Override + protected void refreshWidgetPositions() { + super.refreshWidgetPositions(); + layout.refreshPositions(); + layout.setPosition((width - layout.getWidth()) / 2, (height - layout.getHeight()) / 2); + } + + @Override + public void renderBackground(DrawContext context, int mouseX, int mouseY, float delta) { + super.renderBackground(context, mouseX, mouseY, delta); + drawPopupBackground(context, layout.getX(), layout.getY(), layout.getWidth(), layout.getHeight()); + } + + private void createLayout(GridWidget.Adder adder) { + ColorPickerWidget colorPicker = new ColorPickerWidget(0, 0, 200, 100); + ARGBTextInput argb = new ARGBTextInput(0, 0, textRenderer, true, false); + addDrawableChild(colorPicker); + addDrawableChild(argb); + + argb.setOnChange(color -> { + colorPicker.setRGBColor(color); + currentColor.first(color); + }); + colorPicker.setOnColorChange((color, mouseRelease) -> { + argb.setARGBColor(color); + currentColor.first(color); + }); + + adder.add(colorPicker, 2); + adder.add(argb, 2); + } + + private void createLayoutGradient(GridWidget.Adder adder) { + ColorPickerWidget colorPickerStart = new ColorPickerWidget(0, 0, 200, 100); + ARGBTextInput argbStart = new ARGBTextInput(0, 0, textRenderer, true, false); + ColorPickerWidget colorPickerEnd = new ColorPickerWidget(0, 0, 200, 100); + ARGBTextInput argbEnd = new ARGBTextInput(0, 0, textRenderer, true, false); + addDrawableChild(colorPickerStart); + addDrawableChild(argbStart); + addDrawableChild(colorPickerEnd); + addDrawableChild(argbEnd); + + argbStart.setOnChange(color -> { + colorPickerStart.setRGBColor(color); + currentColor.first(color); + }); + colorPickerStart.setOnColorChange((color, mouseRelease) -> { + argbStart.setARGBColor(color); + currentColor.first(color); + }); + argbEnd.setOnChange(color -> { + colorPickerEnd.setRGBColor(color); + currentColor.second(color); + }); + colorPickerEnd.setOnColorChange((color, mouseRelease) -> { + argbEnd.setARGBColor(color); + currentColor.second(color); + }); + + addDrawableChild(adder.add(new TextWidget(Text.translatable("skyblocker.customItemNames.screen.gradientStart"), textRenderer))); + addDrawableChild(adder.add(new TextWidget(Text.translatable("skyblocker.customItemNames.screen.gradientEnd"), textRenderer))); + + adder.add(colorPickerStart); + adder.add(colorPickerEnd); + adder.add(argbStart); + adder.add(argbEnd); + } + + @FunctionalInterface + public interface GradientConsumer { + void accept(int start, int end); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/CustomizeNameScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/CustomizeNameScreen.java new file mode 100644 index 00000000..f643763b --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/name/CustomizeNameScreen.java @@ -0,0 +1,464 @@ +package de.hysky.skyblocker.skyblock.item.custom.screen.name; + +import de.hysky.skyblocker.config.ConfigUtils; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.debug.Debug; +import de.hysky.skyblocker.skyblock.item.custom.screen.name.visitor.*; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.OkLabColor; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.tooltip.Tooltip; +import net.minecraft.client.gui.widget.*; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.sound.SoundManager; +import net.minecraft.item.ItemStack; +import net.minecraft.text.*; +import net.minecraft.util.*; +import net.minecraft.util.math.ColorHelper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFW; + +import java.util.function.Predicate; + +public class CustomizeNameScreen extends Screen { + private static final int BORDER_SIZE = 12; + private static final Identifier BACKGROUND_TEXTURE = Identifier.ofVanilla("popup/background"); + + private final String uuid; + + private Text text = Text.empty(); + private String textString = ""; + + private TextField textField; + private FormattingButton[] formattingButtons; + + private final GridWidget grid = new GridWidget(); + + private int selectionStart; + private int selectionEnd; + + private @Nullable Style insertAs; + + public CustomizeNameScreen(@NotNull ItemStack stack) { + super(Text.literal("Customize Item Name")); + uuid = ItemUtils.getItemUuid(stack); + setText(stack.getName().copy()); + } + + @Override + protected void init() { + if (uuid.isEmpty()) { + close(); + return; + } + // the gui is a grid of 20 columns, should be 16 px each + textField = grid.add(new TextField(), 1, 0, 1, 20); + addDrawableChild(textField); + formattingButtons = new FormattingButton[]{ + new FormattingButton("B", Formatting.BOLD, Style::isBold), + new FormattingButton("I", Formatting.ITALIC, Style::isItalic), + new FormattingButton("U", Formatting.UNDERLINE, Style::isUnderlined), + new FormattingButton("S", Formatting.STRIKETHROUGH, Style::isStrikethrough), + new FormattingButton("|||", Formatting.OBFUSCATED, Style::isObfuscated), + }; + for (int i = 0; i < formattingButtons.length; i++) { + FormattingButton button = formattingButtons[i]; + addDrawableChild(grid.add(button, 0, i)); + } + + int i = 0; + for (Formatting formatting : Formatting.values()) { + if (formatting.isColor()) { + addDrawableChild(grid.add(new ColorButton(formatting), 2, i++)); + } + } + + assert client != null; + addDrawableChild(grid.add(ButtonWidget.builder(Text.translatable("skyblocker.customItemNames.screen.customColor"), b -> + client.setScreen(ColorPopup.create(this, color -> setStyle(Style.EMPTY.withColor(color)))) + ).size(48, 16).build(), 2, 17, 1, 3)); + addDrawableChild(grid.add(ButtonWidget.builder(Text.translatable("skyblocker.customItemNames.screen.gradientColor"), b -> + client.setScreen(ColorPopup.createGradient(this, this::createGradient)) + ).size(48, 16).build(), 3, 17, 1, 3)); + addDrawableChild(grid.add(ButtonWidget.builder(Text.translatable("gui.cancel"), b -> close()).width(80).build(), 4, 0, 1, 10, Positioner.create().alignRight())); + addDrawableChild(grid.add(ButtonWidget.builder(Text.translatable("gui.done"), b -> { + SkyblockerConfigManager.update(config -> { + if (textString.isBlank()) config.general.customItemNames.remove(uuid); + else config.general.customItemNames.put(uuid, text.copy().setStyle(Style.EMPTY.withItalic(false).withColor(Formatting.WHITE))); + }); + close(); + }).width(80).build(), 4, 10, 1, 10, Positioner.create().alignLeft())); + addDrawableChild(grid.add(new TextWidget(20 * 16, textRenderer.fontHeight, Text.translatable("skyblocker.customItemNames.screen.howToRemove").formatted(Formatting.ITALIC, Formatting.GRAY), textRenderer).alignLeft(), 5, 0, 1, 20, Positioner.create().marginTop(2))); + refreshWidgetPositions(); + } + + @Override + protected void refreshWidgetPositions() { + grid.refreshPositions(); + grid.setPosition((width - grid.getWidth()) / 2, (height - grid.getHeight()) / 2); + } + + @Override + public void renderBackground(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + context.drawGuiTexture( + RenderLayer::getGuiTextured, + BACKGROUND_TEXTURE, + grid.getX() - BORDER_SIZE, + grid.getY() - BORDER_SIZE, + grid.getWidth() + BORDER_SIZE * 2, + grid.getHeight() + BORDER_SIZE * 2); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + super.render(context, mouseX, mouseY, deltaTicks); + // little preview + context.drawCenteredTextWithShadow(textRenderer, text, width / 2, grid.getY() + grid.getHeight() + BORDER_SIZE + 6, -1); + if (Debug.debugEnabled()) { + context.drawTextWithShadow(textRenderer, Text.literal("Selection Start: " + selectionStart + ", Selection End: " + selectionEnd), 10, 10, -1); + context.drawTextWithShadow(textRenderer, Text.literal("Insert Style: " + (insertAs == null ? "null" : insertAs.toString())), 10, 20, -1); + } + } + + /** + * Creates a gradient that goes from {@link CustomizeNameScreen#selectionStart} to {@link CustomizeNameScreen#selectionEnd} + * @param startColor the color at the start of the gradient + * @param endColor the color at the end of the gradient + */ + private void createGradient(int startColor, int endColor) { + int previousSelectionStart = selectionStart; + int previousSelectionEnd = selectionEnd; + + int selStart = Math.min(selectionStart, selectionEnd); + int selSize = Math.abs(selectionEnd - selectionStart); + if (selSize == 0) return; + if (selSize == 1) { + setStyle(Style.EMPTY.withColor(startColor)); + } else { + for (int i = 0; i < selSize; i++) { + selectionStart = selStart + i; + selectionEnd = selStart + i + 1; + int color = OkLabColor.interpolate(startColor, endColor, (float) i / (selSize - 1)); + setStyle(Style.EMPTY.withColor(color)); + } + } + selectionStart = previousSelectionStart; + selectionEnd = previousSelectionEnd; + } + + /** + * Sets the style of the selected text or the insert position if no text is selected. + * @param style the style to set + */ + private void setStyle(Style style) { + if (selectionStart == selectionEnd) { + insertAs = style.withParent(insertAs == null ? Style.EMPTY : insertAs); + return; + } + SetStyleVisitor setStyleVisitor = new SetStyleVisitor(style, selectionStart, selectionEnd); + text.visit(setStyleVisitor, Style.EMPTY); + setText(setStyleVisitor.getNewText()); + } + + private void updateStyleButtons() { + GetStyleVisitor styleVisitor = new GetStyleVisitor(selectionStart, selectionEnd); + text.visit(styleVisitor, Style.EMPTY); + Style style = styleVisitor.getStyle(); + for (FormattingButton button : formattingButtons) { + button.update(style); + } + } + + /** + * Sets the text to be displayed in the text field and updates the textString to avoid calling getString() every time. + * @param text the text to set + */ + public void setText(Text text) { + this.text = text; + textString = text.getString(); + // called before init + if (textField != null) textField.updateMePrettyPlease = true; + } + + @Override + public boolean charTyped(char chr, int modifiers) { + if (super.charTyped(chr, modifiers) || textField.isFocused()) return true; + setFocused(textField); + return textField.charTyped(chr, modifiers); + } + + /** + * Inserts the given text at the current cursor position or replaces the selected text. + * If the text field is empty, it sets the text to the given string. + * @param str the text to insert + */ + public void insertText(String str) { + str = StringHelper.stripInvalidChars(str); + if (text.getContent() == PlainTextContent.EMPTY) { + setText(Text.literal(str).setStyle(insertAs != null ? insertAs : Style.EMPTY)); + } else { + InsertTextVisitor visitor = new InsertTextVisitor(str, insertAs, selectionStart, selectionEnd); + text.visit(visitor, Style.EMPTY); + setText(visitor.getNewText()); + insertAs = null; + } + + selectionStart = Math.min(selectionStart, selectionEnd) + str.length(); + selectionEnd = selectionStart; + updateStyleButtons(); + } + + /** + * Moves the cursor left or right, depending on the direction. + * If shift is held, it will extend the selection. + * If ctrl is held, it will skip to the next word. + * + * @param left whether to move left or right + * @param shiftHeld whether shift is held + * @param ctrlHeld whether ctrl is held + */ + private void moveCursor(boolean left, boolean shiftHeld, boolean ctrlHeld) { + if (left && selectionStart == 0 || (!left && selectionStart == textString.length())) return; + if (ctrlHeld) { + selectionStart = getWordSkipPosition(left); + } else { + selectionStart = Util.moveCursor(textString, selectionStart, left ? -1 : 1); + } + if (!shiftHeld) selectionEnd = selectionStart; + insertAs = null; + updateStyleButtons(); + } + + /** + * Erases the text at the current cursor position or the selected text. + * If the selection is not empty, it will remove the selected text. + * If the selection is empty, it will erase one character or word in the requested direction. + * + * @param left whether to erase left or right + * @param ctrlHeld whether ctrl is held + */ + private void erase(boolean left, boolean ctrlHeld) { + if (selectionStart != selectionEnd) { + insertText(""); + return; + } + moveCursor(left, true, ctrlHeld); + insertText(""); + } + + /** + * Skips one word in the requested direction from selectionStart + * + * @param left the direction + * @return the new position + */ + private int getWordSkipPosition(boolean left) { + int i = selectionStart; + + if (!left) { + int l = this.textString.length(); + i = this.textString.indexOf(32, i); + if (i == -1) { + i = l; + } else { + while (i < l && this.textString.charAt(i) == ' ') { + i++; + } + } + } else { + while (i > 0 && this.textString.charAt(i - 1) == ' ') { + i--; + } + + while (i > 0 && this.textString.charAt(i - 1) != ' ') { + i--; + } + } + + return i; + } + + private class FormattingButton extends PressableWidget { + private boolean enabled; + private final Formatting format; + private final Predicate